Comparing version 1.11.0 to 1.11.1
@@ -0,1 +1,11 @@ | ||
1.11.1 / 2021-09-28 | ||
=================== | ||
This version fixes lots of issues regarding logical operator in object conditions. | ||
* fix: logical operator with multiple conditions such as (#190) | ||
* fix: sequelize mode support HAVING, and select fields raw sql support (#187) | ||
* fix: support len validator (#188) | ||
* fix: normalize logical operator conditions before formatting with spellbook (#186) | ||
1.11.0 / 2021-09-24 | ||
@@ -2,0 +12,0 @@ =================== |
{ | ||
"name": "leoric", | ||
"version": "1.11.0", | ||
"version": "1.11.1", | ||
"description": "JavaScript Object-relational mapping alchemy", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -7,3 +7,3 @@ 'use strict'; | ||
function translateOptions(spell, options) { | ||
const { attributes, where, group, order, offset, limit, include } = options; | ||
const { attributes, where, group, order, offset, limit, include, having } = options; | ||
@@ -15,3 +15,8 @@ if (attributes) spell.$select(attributes); | ||
if (where) spell.$where(where); | ||
if (group) spell.$group(group); | ||
if (group) { | ||
if (Array.isArray(group)) spell.$group(...group); | ||
else spell.$group(group); | ||
} | ||
if (having) spell.$having(having); | ||
if (order) { | ||
@@ -18,0 +23,0 @@ if (typeof order === 'string') { |
@@ -170,3 +170,3 @@ 'use strict'; | ||
const params = args.map(arg => { | ||
return isLogicalOp(ast) && isLogicalOp(arg) && precedes(name, arg.name) <= 0 | ||
return isLogicalOp(ast) && isLogicalOp(arg) && precedes(name, arg.name) < 0 | ||
? `(${formatExpr(spell, arg)})` | ||
@@ -207,53 +207,2 @@ : formatExpr(spell, arg); | ||
} | ||
} | ||
else if (args[1].type == 'op' && Array.isArray(args[1].args) && !isLogicalOp(ast) && isLogicalOp(args[1])) { | ||
let innerOp = args[1].name; | ||
if ([ 'or', 'and' ].includes(innerOp)) { | ||
// { title: { $or: [ 'Leah', 'Diablo' ] } } | ||
// { title: { $or: [ 'Leah', { $like: '%jjj' } ] } } | ||
const expr = []; | ||
const leftValue = { type: 'id', value: args[0].value }; | ||
for (const arg of args[1].args) { | ||
if (arg.type === 'literal') { | ||
const innerAst = { | ||
type: 'op', | ||
name: '=', | ||
args: [ leftValue, arg ], | ||
}; | ||
expr.push(formatExpr(spell, innerAst)); | ||
} else { | ||
arg.args[0] = leftValue; | ||
expr.push(formatExpr(spell, arg)); | ||
} | ||
} | ||
return `(${expr.join(` ${innerOp.toUpperCase()} `)})`; | ||
} else if ('not' === innerOp) { | ||
// { title: { $not: [ 'Leah', 'jss' ] } } | ||
const expr = []; | ||
const leftValue = { type: 'id', value: args[0].value }; | ||
// if all args are literal, it should be `NOT IN (?, ?, ?)` | ||
const notAllLiteral = args[1].args.find(arg => arg.type !== 'literal'); | ||
if (!notAllLiteral) { | ||
const values = args[1].args.map(arg => arg.value); | ||
return `(NOT IN (${values.map(v => { | ||
if (v == null) return 'NULL'; | ||
return '?'; | ||
})}))`; | ||
} | ||
for (const arg of args[1].args) { | ||
if (arg.type === 'literal') { | ||
const innerAst = { | ||
type: 'op', | ||
name: '=', | ||
args: [ leftValue, arg ], | ||
}; | ||
expr.push(formatExpr(spell, innerAst)); | ||
} | ||
else { | ||
arg.args[0] = leftValue; | ||
expr.push(formatExpr(spell, arg)); | ||
} | ||
} | ||
return `(NOT (${expr.join(' AND ')}))`; | ||
} | ||
} else if (params[1] !== '') { | ||
@@ -260,0 +209,0 @@ return `${params[0]} ${name.toUpperCase()} ${params[1]}`; |
122
src/spell.js
@@ -51,9 +51,10 @@ 'use strict'; | ||
function parseConditions(conditions, ...values) { | ||
if (conditions.__raw) return [ conditions ]; | ||
if (isPlainObject(conditions)) { | ||
return parseObjectConditions(conditions); | ||
} | ||
else if (typeof conditions == 'string') { | ||
} else if (typeof conditions == 'string') { | ||
return [parseExpr(conditions, ...values)]; | ||
} | ||
else { | ||
} else if (Array.isArray(conditions)) { | ||
return [parseExpr(...conditions)]; | ||
} else { | ||
throw new Error(`unexpected conditions ${conditions}`); | ||
@@ -65,4 +66,4 @@ } | ||
* Parse object values as literal or subquery | ||
* @param {*} value | ||
* @returns {Object} | ||
* @param {Object} value | ||
* @returns {Array<Object>} | ||
*/ | ||
@@ -73,6 +74,3 @@ function parseObjectValue(value) { | ||
// value maybe an object conditions | ||
if (isPlainObject(value)) { | ||
const [ args ] = parseObjectConditions(value); | ||
return args; | ||
} | ||
if (isPlainObject(value)) return parseObjectConditions(value); | ||
return parseExpr('?', value); | ||
@@ -135,6 +133,13 @@ } | ||
function isLogicalOperator(condition) { | ||
return LOGICAL_OPERATOR_MAP.hasOwnProperty(condition); | ||
function isLogicalOperator(operator) { | ||
return LOGICAL_OPERATOR_MAP.hasOwnProperty(operator); | ||
} | ||
function isLogicalCondition(condition) { | ||
for (const name in condition) { | ||
if (LOGICAL_OPERATOR_MAP.hasOwnProperty(name)) return true; | ||
} | ||
return false; | ||
} | ||
function parseLogicalObjectConditionValue(value) { | ||
@@ -152,2 +157,9 @@ if (value == null || typeof value !== 'object') { | ||
function combineMultipleConditions(result) { | ||
return result.reduce((res, arg) => { | ||
if (!res) return arg; | ||
return { type: 'op', name: 'and', args: [ res, arg ] }; | ||
}); | ||
} | ||
/** | ||
@@ -157,19 +169,26 @@ * @example | ||
* { $or: [ { title: 'Leah' }, { content: 'Diablo' } ] } | ||
* { title: { $or: [ 'Leah', 'Diablo' ] } } | ||
* { title: { $or: [ 'Leah', { $like: '%jjj' } ] } } | ||
* { title: { $not: [ 'Leah', 'jss' ] } } | ||
* @param {string} name logical operators, such as `$or`, `$and` | ||
* { $or: [ 'Leah', 'Diablo' ] } | ||
* { $or: [ 'Leah', { $like: '%jjj' } ] } | ||
* { $not: [ 'Leah', 'jss' ] } | ||
* @param {string} $op logical operators, such as `$or`, `$and` | ||
* @param {Object|Object[]} value logical operands | ||
*/ | ||
function parseLogicalObjectCondition(name, value) { | ||
const operator = LOGICAL_OPERATOR_MAP[name]; | ||
function parseLogicalObjectCondition($op, value) { | ||
const operator = LOGICAL_OPERATOR_MAP[$op]; | ||
const conditions = parseLogicalObjectConditionValue(value); | ||
// { $not: [ 1, 2, 3 ] } | ||
if (operator === 'not' && conditions.every(entry => !isPlainObject(entry))) { | ||
return { type: 'op', name: 'not in', args: [ { type: 'literal', value: conditions } ] }; | ||
} | ||
const args = conditions.reduce((res, condition) => { | ||
if (!isPlainObject(condition)) { | ||
return res.concat({ | ||
type: 'literal', | ||
value: condition, | ||
type: 'op', | ||
name: '=', | ||
args: [ { type: 'literal', value: condition } ], | ||
}); | ||
} | ||
const [ arg ] = parseObjectConditions(condition); | ||
const arg = combineMultipleConditions(parseObjectConditions(condition)); | ||
if (res.length >= 2) { | ||
@@ -184,5 +203,12 @@ return [ { type: 'op', name: operator, args: res }, arg ]; | ||
} | ||
// malformed logical condition: { title: { $or: [ 'Leah' ] } } | ||
// { title: { $or: [ 'Leah' ] } } | ||
if (args.length === 1 && operator !== 'not') return args[0]; | ||
// { title: { $not: [ { $like: '%foo%' }, { $like: '%bar%' } ] } } | ||
if (args.length > 1 && operator === 'not') { | ||
const result = combineMultipleConditions(args); | ||
return { type: 'op', name: operator, args: [ result ] }; | ||
} | ||
return { type: 'op', name: operator, args }; | ||
@@ -192,2 +218,34 @@ } | ||
/** | ||
* normalize logical condition objects like below: | ||
* | ||
* { createdAt: { $not: { $gt: '2021-01-01', $lte: '2021-12-31' } } } | ||
* { createdAt: { $not: [ '2021-09-30', { $gte: '2021-10-07' } ] } } | ||
* { title: { $not: [ 'Leah', 'Nephalem' ] } } | ||
* | ||
* into following format | ||
* | ||
* { $not: [] } | ||
* @param {Object} condition logical condition ast | ||
* @param {string} name expression as the left operand | ||
*/ | ||
function normalizeLogicalCondition(condition, name) { | ||
let args; | ||
if (condition.name === 'not') { | ||
args = condition.args[0].args; | ||
} else { | ||
args = condition.args; | ||
} | ||
if (args.length === 1) { | ||
args.unshift(parseExpr(name)); | ||
} else { | ||
for (let i = 0; i < args.length; i++) { | ||
const arg = args[i]; | ||
if (arg.args && arg.args.length === 1) { | ||
arg.args.unshift(parseExpr(name)); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* parse conditions in MongoDB style, which is quite polular in ORMs for JavaScript. See {@link module:src/spell~OPERATOR_MAP} for supported `$op`s. | ||
@@ -220,13 +278,16 @@ * @example | ||
// if name is a common operator | ||
/** | ||
* { | ||
* $like: '%no%' | ||
* } | ||
*/ | ||
// { $like: '%no%' } | ||
result.push({ | ||
type: 'op', | ||
name: OPERATOR_MAP[name], | ||
args: [ parseExpr(name), parseObjectValue(value) ], | ||
args: [ parseObjectValue(value) ], | ||
}); | ||
} | ||
else if (isLogicalCondition(value)) { | ||
const conds = parseObjectValue(value); | ||
for (const condition of conds) { | ||
normalizeLogicalCondition(condition, name); | ||
result.push(condition); | ||
} | ||
} | ||
else { | ||
@@ -254,3 +315,4 @@ result.push({ | ||
for (const name of names) { | ||
columns.push(...parseExprList(name)); | ||
if (name.__raw) columns.push(name); | ||
else columns.push(...parseExprList(name)); | ||
} | ||
@@ -928,3 +990,3 @@ | ||
// https://stackoverflow.com/questions/32730296/referring-to-a-select-aggregate-column-alias-in-the-having-clause-in-postgres | ||
if (this.Model.driver.type === 'postgres') { | ||
if (this.Model.driver.type === 'postgres' && !condition.__raw) { | ||
const { value } = condition.args[0]; | ||
@@ -931,0 +993,0 @@ for (const column of this.columns) { |
@@ -59,2 +59,5 @@ 'use strict'; | ||
}, | ||
len(str, min, max) { | ||
return Validator.isLength(str, min, max); | ||
} | ||
}; | ||
@@ -61,0 +64,0 @@ |
261510
7534