From 2324831c372d43086f13c5b517cddd5c02f81a20 Mon Sep 17 00:00:00 2001 From: James Ferguson Date: Sun, 10 Jun 2018 18:30:03 +1000 Subject: [PATCH] feat(OrderBy): add new order by constraint style Fixes #9 --- src/builder.ts | 50 +++++++++++++++++++++++------------- src/clauses/order-by.spec.ts | 9 +++++++ src/clauses/order-by.ts | 35 +++++++++++++++++-------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/builder.ts b/src/builder.ts index 68559012..2af85fc7 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -5,7 +5,7 @@ import { } from './clauses'; import { DeleteOptions } from './clauses/delete'; import { MatchOptions } from './clauses/match'; -import { Direction, OrderConstraints } from './clauses/order-by'; +import { Direction, OrderConstraint, OrderConstraints } from './clauses/order-by'; import { PatternCollection } from './clauses/pattern-clause'; import { SetOptions, SetProperties } from './clauses/set'; import { Term } from './clauses/term-list-clause'; @@ -383,43 +383,57 @@ export abstract class Builder extends SetBlock { * https://neo4j.com/docs/developer-manual/current/cypher/clauses/order-by} * to the query. * - * You can supply a single string or an array of strings to order by and the - * direction parameter can control which way all fields are sorted by. + * Pass a single string or an array of strings to order by. * ```javascript * query.orderBy([ * 'name', * 'occupation', - * ], 'DESC') + * ]) + * // ORDER BY name, occupation * ``` * - * Results in a query of + * You can control the sort direction by adding a direction to each property. + * ```javascript + * query.orderBy([ + * ['name', 'DESC'], + * 'occupation', // Same as ['occupation', 'ASC'] + * ]) + * // ORDER BY name DESC, occupation * ``` - * ORDER BY name DESC, occupation DESC + * + * The second parameter is the default search direction for all properties that + * don't have a direction specified. So the above query could instead be + * written as: + * ```javascript + * query.orderBy([ + * 'name', + * ['occupation', 'ASC'] + * ], 'DESC') + * // ORDER BY name DESC, occupation * ``` * - * If you would like to control the direction on each property individually, - * you can provide an object where each key is the property and the value is a - * direction. Eg: + * Valid values for directions are `DESC`, `DESCENDING`, `ASC`, `ASCENDING`. + * `true` and `false` are also accepted (`true` being the same as `DESC` and + * `false` the same as `ASC`), however they should be avoided as they are + * quite ambiguous. Directions always default to `ASC` as it does in cypher. + * + * *Depreciation note:* + * It was previously acceptable to pass an object where each key is the + * property and the value is a direction. Eg: * ```javascript * query.orderBy({ * name: 'DESC', * occupation: 'ASC', * }) * ``` - * - * Results in a query of - * ``` - * ORDER BY name DESC, occupation - * ``` - * - * Direction defaults to `ASC` as it does in cypher. - * + * This has been deprecated as the iteration order of objects is not + * always consistent. * * @param {_.Many | OrderConstraints} fields * @param {Direction} dir * @returns {Q} */ - orderBy(fields: Many | OrderConstraints, dir?: Direction) { + orderBy(fields: string | (string | OrderConstraint)[] | OrderConstraints, dir?: Direction) { return this.continueChainClause(new OrderBy(fields, dir)); } diff --git a/src/clauses/order-by.spec.ts b/src/clauses/order-by.spec.ts index b87ffe68..bcaf6958 100644 --- a/src/clauses/order-by.spec.ts +++ b/src/clauses/order-by.spec.ts @@ -43,4 +43,13 @@ describe('OrderBy', () => { }); expect(query.build()).to.equal('ORDER BY node.prop1 DESC, node.prop2, node.prop3 DESC'); }); + + it('should support multiple order columns with directions using the array syntax', () => { + const query = new OrderBy([ + ['node.prop1', 'DESC'], + 'node.prop2', + ['node.prop3', true], + ]); + expect(query.build()).to.equal('ORDER BY node.prop1 DESC, node.prop2, node.prop3 DESC'); + }); }); diff --git a/src/clauses/order-by.ts b/src/clauses/order-by.ts index 7596cbe8..b880b033 100644 --- a/src/clauses/order-by.ts +++ b/src/clauses/order-by.ts @@ -1,33 +1,46 @@ import { Clause } from '../clause'; -import { join, map, Many, isString, isArray, Dictionary, reduce, mapValues, assign } from 'lodash'; +import { join, map, isString, isArray, Dictionary, trim } from 'lodash'; export type Direction = boolean | 'DESC' | 'DESCENDING' | 'ASC' | 'ASCENDING'; +export type InternalDirection = 'DESC' | ''; +export type OrderConstraint = [string, Direction] | [string]; +export type InternalOrderConstraint = { field: string, direction: InternalDirection }; export type OrderConstraints = Dictionary; export class OrderBy extends Clause { - constraints: Dictionary<'DESC' | ''>; + constraints: InternalOrderConstraint[]; - constructor(fields: Many | OrderConstraints, dir?: Direction) { + constructor(fields: string | (string | OrderConstraint)[] | OrderConstraints, dir?: Direction) { super(); - const reverse = OrderBy.normalizeDirection(dir); + const direction = OrderBy.normalizeDirection(dir); if (isString(fields)) { - this.constraints = { [fields]: reverse }; + this.constraints = [{ direction, field: fields }]; } else if (isArray(fields)) { - this.constraints = reduce(fields, (obj, field) => assign(obj, { [field]: reverse }), {}); + this.constraints = map(fields, (field): InternalOrderConstraint => { + if (!isArray(field)) { + return { field, direction }; + } + const fieldDirection = field[1] ? OrderBy.normalizeDirection(field[1]) : direction; + return { field: field[0], direction: fieldDirection }; + }); } else { - this.constraints = mapValues(fields, OrderBy.normalizeDirection); + // tslint:disable-next-line:max-line-length + console.warn('Warning: Passing constraints to OrderBy using an object is deprecated as the iteration order is not guaranteed. Use an array instead.'); + this.constraints = map(fields, (fieldDirection, field) => { + return { field, direction: OrderBy.normalizeDirection(fieldDirection) }; + }); } } build() { - const contraints = map(this.constraints, (dir, prop) => { - return prop + (dir.length > 0 ? ` ${dir}` : ''); + const constraints = map(this.constraints, ({ field, direction }) => { + return trim(`${field} ${direction}`); }); - return 'ORDER BY ' + join(contraints, ', '); + return 'ORDER BY ' + join(constraints, ', '); } - private static normalizeDirection(dir?: Direction): 'DESC' | '' { + private static normalizeDirection(dir?: Direction | string): InternalDirection { const isDescending = dir === 'DESC' || dir === 'DESCENDING' || dir === true; return isDescending ? 'DESC' : ''; }