Skip to content

Commit

Permalink
New: Spell#orWhere() and Spell#orHaving()
Browse files Browse the repository at this point in the history
  • Loading branch information
cyjake committed Apr 24, 2019
1 parent 0268703 commit b029529
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 14 deletions.
7 changes: 4 additions & 3 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
0.4.2 / 2019-03-29
==================

* New: `Spell#orWhere()` and `Spell#orHaving()`
* New: arithmetic operators
* New: unary operators such as unary minus `-` and bit invertion `~`
* Fix: unset attribute should be overwritable
* Fix: `attributeChanged()` should be false if attribute is unset and not overwritten
* Fix: subclass with incomplete getter/setter should be complemented
Expand All @@ -10,9 +13,7 @@
* Fix: `INSERT ... UPDATE` with `id = LAST_INSERT_ID(id)` in MySQL
* Fix: `Model.find({ name: { $op1, $op2 } })` object conditions with multiple operators
* Fix: prefixing result set with qualifiers if query contains join relations and is not dispatchable
* Fix: arithmetic operators
* Fix: unary operators
* Fix: `Spell.$get(index)` with LIMIT
* Fix: `Spell#$get(index)` with LIMIT
* Docs: `Model.transaction()`
* Docs: definition types with `index.d.ts`

Expand Down
8 changes: 8 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,17 @@ declare class Spell {
where(conditions: string, ...values: Literal[]): Spell & Promise<Bone>;
where(conditions: WhereConditions): Spell & Promise<Bone>;

orWhere(conditions: string, ...values: Literal[]): Spell & Promise<Bone>;
orWhere(conditions: WhereConditions): Spell & Promise<Bone>;

group(...names: string[]): Spell & Promise<ResultSet>;

having(conditions: string, ...values: Literal[]): Spell & Promise<ResultSet>;
having(conditions: WhereConditions): Spell & Promise<ResultSet>;

orHaving(conditions: string, ...values: Literal[]): Spell & Promise<ResultSet>;
orHaving(conditions: WhereConditions): Spell & Promise<ResultSet>;

order(name: string, order?: 'desc' | 'asc'): Spell & Promise<Bone>;
order(opts: OrderOptions): Spell & Promise<Bone>;

Expand Down
2 changes: 1 addition & 1 deletion lib/expr.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,4 @@ function parseExpr(str, ...values) {
return parseExprList(str, ...values)[0]
}

module.exports = { parseExpr, parseExprList }
module.exports = { parseExpr, parseExprList, precedes }
45 changes: 35 additions & 10 deletions lib/spell.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
const pluralize = require('pluralize')
const SqlString = require('sqlstring')
const { parseExprList, parseExpr } = require('./expr')
const { parseExprList, parseExpr, precedes } = require('./expr')

const OPERATOR_MAP = {
$between: 'between',
Expand Down Expand Up @@ -37,11 +37,10 @@ function isPlainObject(value) {
}

/**
* Allows two types of params:
*
* parseConditions({ foo: { $op: value } })
* parseConditions('foo = ?', value)
*
* Parse condition expressions
* @example
* parseConditions({ foo: { $op: value } })
* parseConditions('foo = ?', value)
* @param {(string|Object)} conditions
* @param {...*} values
* @returns {Array}
Expand Down Expand Up @@ -315,7 +314,7 @@ function isLogicalOp({ type, name }) {
function formatOpExpr(spell, ast) {
const { name, args } = ast
const params = args.map(arg => {
return isLogicalOp(ast) && isLogicalOp(arg)
return isLogicalOp(ast) && isLogicalOp(arg) && precedes(name, arg.name) <= 0
? `(${formatExpr(spell, arg)})`
: formatExpr(spell, arg)
})
Expand Down Expand Up @@ -667,7 +666,7 @@ function formatDelete(spell) {
function formatConditions(spell, conditions) {
return conditions
.map(condition => {
return isLogicalOp(condition) && condition.name == 'or'
return isLogicalOp(condition) && condition.name == 'or' && conditions.length > 1
? `(${formatExpr(spell, condition)})`
: formatExpr(spell, condition)
})
Expand Down Expand Up @@ -1223,6 +1222,19 @@ class Spell {
return this
}

$orWhere(conditions, ...values) {
const { whereConditions } = this
if (whereConditions.length == 0) return this.$where(conditions, ...values)
const combined = whereConditions.slice(1).reduce((result, condition) => {
return { type: 'op', name: 'and', args: [result, condition] }
}, whereConditions[0])
this.whereConditions = [
{ type: 'op', name: 'or', args:
[combined, ...parseConditions(conditions, ...values)] }
]
return this
}

/**
* Set GROUP BY attributes. `select_expr` with `AS` is supported, hence following expressions have the same effect:
*
Expand Down Expand Up @@ -1316,8 +1328,8 @@ class Spell {
* @param {...*} values
* @returns {Spell}
*/
$having(conditions, values) {
for (const condition of parseConditions(conditions, values)) {
$having(conditions, ...values) {
for (const condition of parseConditions(conditions, ...values)) {
// Postgres can't have alias in HAVING caluse
// https://stackoverflow.com/questions/32730296/referring-to-a-select-aggregate-column-alias-in-the-having-clause-in-postgres
if (this.Model.pool.Leoric_type === 'pg') {
Expand All @@ -1334,6 +1346,19 @@ class Spell {
return this
}

$orHaving(conditions, ...values) {
this.$having(conditions, ...values)
const { havingConditions } = this
const len = havingConditions.length
const condition = havingConditions.slice(1, len - 1).reduce((result, condition) => {
return { type: 'op', name: 'and', args: [result, condition] }
}, havingConditions[0])
this.havingConditions = [
{ type: 'op', name: 'or', args: [condition, havingConditions[len - 1]] }
]
return this
}

/**
* LEFT JOIN predefined associations in model.
* @example
Expand Down
13 changes: 13 additions & 0 deletions test/suite/querying.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ describe('=> Where', function() {
expect(posts[1].title).to.equal('Archbishop Lazarus')
expect(posts[1].authorId).to.equal(2)
})

it('.orWhere(query, ...values)', async function() {
const posts = await Post.where('title = ?', 'New Post').orWhere('title = ?', 'Skeleton King').order('title')
assert.deepEqual(Array.from(posts, post => post.title), ['New Post', 'Skeleton King'])
})
})

describe('=> Select', function() {
Expand Down Expand Up @@ -393,6 +398,14 @@ describe('=> Count / Group / Having', function() {
{ count: 2, title: 'New Post' }
])
})

it('Bone.group().having().orHaving()', async function() {
assert.deepEqual(
await Post.group('title').count().having('count > 1').orHaving('title = ?', 'Archangel Tyrael').order('count', 'desc'),
[ { count: 2, title: 'New Post' },
{ count: 1, title: 'Archangel Tyrael'} ]
)
})
})

describe('=> Group / Join / Subqueries', function() {
Expand Down
14 changes: 14 additions & 0 deletions test/test.spell.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ describe('=> Select', function() {
)
})

it('orWhere', function() {
assert.equal(
Post.where({ id: 1 }).where('title = ?', 'New Post').orWhere('title = ?', 'Leah').toString(),
"SELECT * FROM `articles` WHERE (`id` = 1 AND `title` = 'New Post' OR `title` = 'Leah') AND `gmt_deleted` IS NULL"
)
})

it('orHaving', function() {
assert.equal(
Post.count().group('authorId').having('count > ?', 10).orHaving('count = 5').toString(),
'SELECT COUNT(*) AS `count`, `author_id` FROM `articles` WHERE `gmt_deleted` IS NULL GROUP BY `author_id` HAVING `count` > 10 OR `count` = 5'
)
})

it('count / group by / having / order', function() {
assert.equal(
Post.group('authorId').count().having({ count: { $gt: 0 } }).order('count desc').toString(),
Expand Down

0 comments on commit b029529

Please sign in to comment.