Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

leoric

Package Overview
Dependencies
Maintainers
1
Versions
141
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

leoric - npm Package Compare versions

Comparing version 0.4.1 to 0.4.2

index.d.ts

19

History.md

@@ -0,1 +1,20 @@

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
* Fix: sharding key validation on `Bone.update()` and `Bone.save()`
* Fix: sharding key should be along with primary key on `bone.remove()`
* Fix: `Bone.cast()` should leave `null` as is
* 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: `Spell#$get(index)` with LIMIT
* Docs: `Model.transaction()`
* Docs: definition types with `index.d.ts`
0.4.1 / 2019-03-21

@@ -2,0 +21,0 @@ ==================

43

index.js

@@ -45,19 +45,2 @@ 'use strict'

async function tableInfo(pool, tables) {
const queries = tables.map(table => {
return pool.Leoric_query(`PRAGMA table_info(${pool.escapeId(table)})`)
})
const results = await Promise.all(queries)
const schema = {}
for (let i = 0; i < tables.length; i++) {
const table = tables[i]
const { rows } = results[i]
const columns = rows.map(({ name, type, notnull, dflt_value, pk }) => {
return { name, type, isNullable: notnull == 1, default: dflt_value }
})
schema[table] = columns
}
return schema
}
function findClient(name) {

@@ -70,4 +53,2 @@ switch (name) {

return require('./lib/clients/pg')
case 'sqlite3':
return require('./lib/clients/sqlite')
default:

@@ -79,2 +60,3 @@ throw new Error(`Unsupported database ${name}`)

async function requireModels(dir) {
if (!dir || typeof dir !== 'string') throw new Error(`Unexpected dir (${dir})`)
const entries = await fs.readdir(dir, { withFileTypes: true })

@@ -99,28 +81,27 @@ const models = []

* @param {string|Bone[]} opts.models - an array of models
* @param {Object} opts.
* @returns {Pool} the connection pool in case we need to perform raw query
*/
const connect = async function Leoric_connect(opts) {
if (Bone.pool) return
if (Bone.pool) {
throw new Error('connected already')
}
opts = Object.assign({ client: 'mysql', database: opts.db }, opts)
const { client, database } = opts
const pool = findClient(client)(opts)
const dir = opts.model || opts.models
const models = Array.isArray(dir) ? dir : (await requireModels(dir))
const Models = Array.isArray(dir) ? dir : (await requireModels(dir))
if (models.length <= 0) throw new Error('Unable to find any models')
if (Models.length <= 0) throw new Error(`Unable to find models (${dir})`)
const pool = findClient(client)(opts)
Bone.model = {}
Bone.pool = pool
Collection.pool = pool
const query = client.includes('sqlite')
? tableInfo(pool, models.map(model => model.physicTable))
: schemaInfo(pool, database, models.map(model => model.physicTable))
const schema = await query
for (const Model of models) {
const schema = await schemaInfo(pool, database, Models.map(Model => Model.physicTable))
for (const Model of Models) {
Model.describeTable(schema[Model.physicTable])
Bone.model[Model.name] = Model
}
Bone.models = models
for (const Model of Bone.models) {
for (const Model of Models) {
Model.describe()

@@ -127,0 +108,0 @@ }

@@ -71,2 +71,5 @@ 'use strict'

* @alias Bone
* @property {Object} raw
* @property {Object} rawOriginal
* @property {Set} rawUnset
* @example

@@ -94,3 +97,3 @@ * const post = new Post()

},
rawMissing: {
rawUnset: {
value: new Set(),

@@ -113,3 +116,3 @@ writable: false,

* const post = Post.select('title').first
* post.createdAt // throw Error('Missing attribute createdAt')
* post.createdAt // throw Error('Unset attribute "createdAt"')
*

@@ -134,8 +137,10 @@ * This is the underlying method of attribute getter/setters:

if (!(name in schema)) {
throw new Error(`${this.constructor.name} has no attribute called ${name}`)
throw new Error(`${this.constructor.name} has no attribute "${name}"`)
}
if (this.rawMissing.has(name)) throw new Error(`Missing attribute ${name}`)
if (arguments.length > 1) {
this.raw[name] = value
this.rawUnset.delete(name)
return this
} else {
if (this.rawUnset.has(name)) throw new Error(`Unset attribute "${name}"`)
const value = this.raw[name]

@@ -156,3 +161,3 @@ // make sure null is returned if value is undefined

attributeWas(name) {
if (this.rawMissing.has(name)) throw new Error(`Missing attribute ${name}`)
if (this.rawUnset.has(name)) throw new Error(`Unset attribute "${name}"`)
const value = this.rawOriginal[name]

@@ -173,2 +178,3 @@ return value == null ? null : value

attributeChanged(name) {
if (this.rawUnset.has(name)) return false
const value = this.attribute(name)

@@ -186,3 +192,3 @@ const valueWas = this.attributeWas(name)

*/
inspect() {
[util.inspect.custom]() {
return this.constructor.name + ' ' + util.inspect(this.toJSON())

@@ -202,3 +208,3 @@ }

for (const name in this.constructor.schema) {
if (!this.rawMissing.has(name) && this[name] != null) {
if (!this.rawUnset.has(name) && this[name] != null) {
obj[name] = this[name]

@@ -208,5 +214,6 @@ }

for (const name in this) {
if (this.hasOwnProperty(name) && this[name] != null) {
obj[name] = this[name]
for (const name of Object.keys(this)) {
const value = this[name]
if (value != null) {
obj[name] = typeof value.toJSON === 'function' ? value.toJSON() : value
}

@@ -229,9 +236,10 @@ }

for (const name in this.constructor.schema) {
if (!this.rawMissing.has(name)) obj[name] = this.attribute(name)
if (!this.rawUnset.has(name)) obj[name] = this.attribute(name)
}
for (const name in this) {
if (this.hasOwnProperty(name)) {
obj[name] = this[name]
}
for (const name of Object.keys(this)) {
const value = this[name]
obj[name] = value != null && typeof value.toObject == 'function'
? value.toObject()
: value
}

@@ -253,12 +261,13 @@

*/
save() {
async save() {
const { primaryKey } = this.constructor
if (this.rawMissing.has(primaryKey)) throw new Error('Missing primary key')
if (this.rawUnset.has(primaryKey)) throw new Error(`Unset primary key ${primaryKey}`)
if (this[primaryKey] == null) {
return this.create().then(() => this)
await this.create()
} else if (this.attributeChanged(primaryKey)) {
return this.upsert().then(() => this)
await this.upsert()
} else {
return this.update().then(() => this)
await this.update()
}
return this
}

@@ -296,7 +305,2 @@

if (schema.createdAt && !this.createdAt) this.createdAt = new Date()
if (schema.updatedAt && !(this.updatedAt && this.attributeChanged('updatedAt'))) {
this.updatedAt = new Date()
}
for (const name in schema) {

@@ -306,6 +310,11 @@ if (this.attributeChanged(name)) data[name] = this.attribute(name)

if (Object.keys(data).length === 0) {
return Promise.resolve()
if (Object.keys(data).length === 0) return Promise.resolve(0)
if (schema.createdAt && !this.createdAt) data.createdAt = new Date()
if (schema.updatedAt && !(this.updatedAt && this.attributeChanged('updatedAt'))) {
data.updatedAt = new Date()
}
if (this[primaryKey]) data[primaryKey] = this[primaryKey]
// About LAST_INSERT_ID()

@@ -330,8 +339,4 @@ // - http://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_last-insert-id

const data = {}
const { schema, primaryKey } = this.constructor
const { schema, primaryKey, shardingKey } = this.constructor
if (schema.updatedAt && !this.updatedAt) {
this.updatedAt = new Date()
}
for (let name in schema) {

@@ -343,5 +348,11 @@ if (this.attributeChanged(name)) {

return this.constructor.update({
[primaryKey]: this[primaryKey]
}, data).then(count => {
if (Object.keys(data).length === 0) return Promise.resolve(0)
if (this[primaryKey] == null) {
throw new Error(`Unable to UPDATE due to unset primary key ${primaryKey}`)
}
const where = { [primaryKey]: this[primaryKey] }
if (shardingKey) where[shardingKey] = this[shardingKey]
return this.constructor.update(where, data).then(count => {
if (count === 1) this.syncRaw()

@@ -390,9 +401,12 @@ return count

const Model = this.constructor
const { primaryKey } = Model
const { primaryKey, shardingKey } = Model
if (!this[primaryKey]) {
if (this[primaryKey] == null) {
throw new Error('The instance is not persisted yet.')
}
return Model.remove({ [primaryKey]: this[primaryKey] }, forceDelete)
const condition = { [primaryKey]: this[primaryKey] }
if (shardingKey) condition[shardingKey] = this[shardingKey]
return Model.remove(condition, forceDelete)
}

@@ -462,2 +476,3 @@

for (const name in schema) {
const descriptor = Object.getOwnPropertyDescriptor(this.prototype, name)
descriptors[name] = Object.assign({

@@ -469,5 +484,7 @@ get() {

this.attribute(name, value)
},
enumerable: true
}, Object.getOwnPropertyDescriptor(this.prototype, name))
}
}, Object.keys(descriptor || {}).reduce((result, key) => {
if (descriptor[key] != null) result[key] = descriptor[key]
return result
}, {}))
}

@@ -588,3 +605,7 @@

static renameAttribute(originalName, newName) {
if (originalName in this.schema) {
if (this.schema.hasOwnProperty(newName)) {
throw new Error(`Unable to override existing attribute "${newName}"`)
}
if (this.schema.hasOwnProperty(originalName)) {
this.schema[newName] = this.schema[originalName]

@@ -607,3 +628,3 @@ this.amehcs[this.schema[newName].column] = newName

/**
* Find model by class name. Models are stored at {@link Bone.models}.
* Find model by class name. Models are stored at {@link Bone.model}.
* @param {string} className

@@ -613,8 +634,5 @@ * @returns {Bone}

static reflectClass(className) {
for (const Model of this.models) {
if (Model.name === className) {
return Model
}
}
throw new Error(`Cannot find Class definition of ${className}`)
const Model = this.model[className]
if (!Model) throw new Error(`Unable to find model "${className}"`)
return Model
}

@@ -652,2 +670,4 @@

static cast(value, type) {
if (value == null) return value
switch (type) {

@@ -779,7 +799,8 @@ case JSON:

for (const qualifier in spell.joins) {
const values = row[qualifier]
const { Model, hasMany } = spell.joins[qualifier]
// It seems mysql2 nests rows with table name instead of qualifier.
const values = row[qualifier] || row[Model.table]
const id = values[Model.primaryColumn]
if (hasMany) {
if (!current[qualifier]) current[qualifier] = []
if (!current[qualifier]) current[qualifier] = new Collection()
if (!id || current[qualifier].some(item => item[Model.primaryKey] == id)) continue

@@ -810,7 +831,8 @@ current[qualifier].push(Model.instantiate(values))

for (const row of rows) {
const result = {}
const result = { '': {} }
for (const qualifier in row) {
const data = row[qualifier]
const obj = result[qualifier] || (result[qualifier] = {})
if (qualifier == '') {
Object.assign(result, data)
Object.assign(obj, data)
}

@@ -821,3 +843,4 @@ else if (qualifier in joins || qualifier == spell.Model.aliasName || qualifier == spell.Model.table) {

const name = Model.amehcs[column]
result[name || column] = data[column]
if (name) obj[name] = data[column]
else result[''][column] = data[column]
}

@@ -832,3 +855,8 @@ }

return results
if (Object.keys(joins).length > 0) return results
return results.map(result => {
const merged = {}
for (const obj of Object.values(result)) Object.assign(merged, obj)
return merged
})
}

@@ -844,3 +872,3 @@

const instance = new this()
const { raw, rawOriginal, rawMissing } = instance
const { raw, rawOriginal, rawUnset } = instance

@@ -854,3 +882,3 @@ for (const name in this.schema) {

} else {
rawMissing.add(name)
rawUnset.add(name)
}

@@ -917,4 +945,3 @@ }

static findOne(conditions, ...values) {
const spell = this.find(conditions, ...values).$limit(1)
return spell.$get(0)
return this.find(conditions, ...values).$get(0)
}

@@ -961,2 +988,7 @@

static update(conditions, values) {
if (Object.keys(values).length <= 0) {
// nothing to update
return Promise.resolve(0)
}
const spell = new Spell(this, async spell => {

@@ -990,15 +1022,11 @@ const result = await this.query(spell, values)

static remove(conditions, forceDelete = false) {
if (forceDelete === true) {
const spell = new Spell(this, async spell => {
const result = await this.query(spell)
return result.affectedRows
})
return spell.unscoped.$where(conditions).$delete()
}
else if (this.schema.deletedAt) {
if (forceDelete !== true && this.schema.deletedAt) {
return this.update(conditions, { deletedAt: new Date() })
}
else {
throw new Error('Soft delete not available.')
}
const spell = new Spell(this, async spell => {
const result = await this.query(spell)
return result.affectedRows
})
return spell.unscoped.$where(conditions).$delete()
}

@@ -1032,4 +1060,4 @@

} catch (err) {
console.error(err.stack)
await connection.Leoric_query('ROLLBACK')
throw err
} finally {

@@ -1036,0 +1064,0 @@ connection.release()

@@ -12,3 +12,3 @@ 'use strict'

toJSON() {
return this.map(function(element) {
return Array.from(this, function(element) {
if (typeof element.toJSON === 'function') {

@@ -26,3 +26,3 @@ return element.toJSON()

toObject() {
return this.map(function(element) {
return Array.from(this, function(element) {
if (typeof element.toObject === 'function') {

@@ -29,0 +29,0 @@ return element.toObject()

@@ -7,3 +7,2 @@ 'use strict'

* @example
* const parseExpr = require('./lib/expr')
* parseExpr('COUNT(1) AS count')

@@ -19,3 +18,3 @@ * // => { type: 'alias', value: 'count',

const UNARY_OPERATORS = [
'not', '!'
'not', '!', '-', '~'
]

@@ -39,2 +38,3 @@

'like',
'+',
'-',

@@ -46,3 +46,6 @@ 'mod', '%',

'or', '||',
'*'
'xor',
'*',
'/',
'^'
]

@@ -58,2 +61,10 @@

const OPERATORS = new Set(
['between', 'not between', ...UNARY_OPERATORS, ...BINARY_OPERATORS].sort((a, b) => {
if (a.length > b.length) return -1
else if (a.length < b.length) return 1
else return 0
})
)
/**

@@ -64,2 +75,4 @@ * A map of operator alias. Operators like `&&` will be translated into `and` to make ast handling a bit easier.

const OPERATOR_ALIAS_MAP = {
'is': '=',
'is not': '!=',
'!': 'not',

@@ -69,3 +82,3 @@ '&&': 'and',

'||': 'or',
'%': 'mod'
'mod': '%'
}

@@ -85,19 +98,34 @@

const PRECEDENCES = [
'not',
'and',
'xor',
'or'
['^'], // left out unary minus on purpose
['*', '/', 'div', '%', 'mod'],
['-', '+'],
['=', '>=', '>', '<=', '<', '<>', '!=', 'like', 'in'],
['not'],
['and'],
['xor'],
['or']
]
/**
* Check if the left operator has higher precedence over the right one. For example:
*
* isEqualOrHigher('and', 'or') // => true
* isEqualOrHigher('and', 'and') // => true
*
* Compare the precedence of two operators. Operators with lower indices have higher priorities.
* @example
* precedes('and', 'or') // => -1
* precedes('and', 'and') // => 0
* precedes('+', '/') // => 1
* @param {string} left - name of the left operator
* @param {string} right - name of the right operator
*/
function isEqualOrHigher(left, right) {
return left == right || PRECEDENCES.indexOf(left) < PRECEDENCES.indexOf(right)
function precedes(left, right) {
if (left == right) return 0
let leftIndex = -1
let rightIndex = -1
for (const i of PRECEDENCES.keys()) {
const operators = PRECEDENCES[i]
if (operators.includes(left)) leftIndex = i
if (operators.includes(right)) rightIndex = i
}
return leftIndex < rightIndex ? -1 : (leftIndex == rightIndex ? 0 : 1)
}

@@ -153,3 +181,3 @@

next()
return { type: 'func', name: name.toLowerCase(), args }
return { type: 'func', name, args }
}

@@ -195,6 +223,15 @@

if (/['"]/.test(chr)) return string()
if (chr == '*') return wildcard()
if (chr == '?') return placeholder()
if (chr == '(') return array()
for (const name of OPERATORS) {
const j = i + name.length
const chunk = str.slice(i, j)
if (chunk.toLowerCase() == name && (str[j] == ' ' || !/[a-z]$/.test(name))) {
i += name.length
chr = str[i]
return { type: 'op', name: OPERATOR_ALIAS_MAP[name] || name, args: [] }
}
}
let value = ''

@@ -205,17 +242,13 @@ while (chr && /[a-z0-9$_.]/i.test(chr)) {

}
const lowerCase = value.toLowerCase()
if (chr == '(') return func(value)
if (chr == '!') {
next()
return { type: 'op', name: 'not', args: [] }
if (!value) {
throw new Error(`Unexpected token ${chr}`)
}
if (chr && !/[), ]/.test(chr)) throw new Error(`Unexpected token ${chr}`)
const lowerCase = value.toLowerCase()
if (UNARY_OPERATORS.includes(lowerCase)) {
return { type: 'op', name: lowerCase, args: [] }
}
else if (MODIFIERS.includes(lowerCase)) {
return { type: 'mod', name: lowerCase, args: [] }
}
else if (chr == '(') {
return func(lowerCase)
}
else if (lowerCase == 'null') {

@@ -232,28 +265,11 @@ return { type: 'literal', value: null }

function keyword() {
let value = ''
while (chr && chr != ' ') {
value += chr
next()
}
if (/^not$/i.test(value) && /^\s+(?:between|in|like)/i.test(str.slice(i))) {
space()
value += ` ${keyword()}`
}
else if (/^is$/i.test(value) && /^\s+not/i.test(str)) {
space()
value += ' not'
}
return OPERATOR_ALIAS_MAP[value] || value.toLowerCase()
}
function between(name, t) {
function between(op, t) {
space()
const start = token()
space()
const conj = keyword()
if (conj != 'and') throw new Error(`Unexpected conj ${conj}`)
const conj = token()
if (conj.name != 'and') throw new Error(`Unexpected conj ${conj}`)
space()
const end = token()
return { type: 'op', name, args: [ t, start, end ] }
return { ...op, args: [ t, start, end ] }
}

@@ -266,25 +282,45 @@

function unary(op) {
const arg = chr == '(' ? expr() : token()
if (op.name == '-' && arg.type == 'literal' && Number.isFinite(arg.value)) {
return { type: 'literal', value: -arg.value }
} else {
return { ...op, args: [arg] }
}
}
function operator(t) {
const name = keyword()
if (name == 'as') return alias(t)
if (name == 'between' || name == 'not between') {
return between(name, t)
const op = token()
if (op.name == 'as') return alias(t)
if (op.name == 'between' || op.name == 'not between') {
return between(op, t)
}
else if (BINARY_OPERATORS.includes(name)) {
if (BINARY_OPERATORS.includes(op.name)) {
space()
const isLower = chr == '('
const operand = LOGICAL_OPERATORS.includes(name) ? expr() : token()
if (operand.type == 'op' && !isLower && isEqualOrHigher(name, operand.name)) {
const operand = LOGICAL_OPERATORS.includes(op.name) ? expr() : token()
// parseExpr('1 > -1')
if (UNARY_OPERATORS.includes(operand.name) && operand.args.length == 0) {
return { ...op, args: [t, unary(operand)] }
}
else if (operand.type == 'op' && operand.args.length < 2) {
throw new Error(`Unexpected token ${operand.name}`)
}
// parseExpr('a = 1 && b = 2 && c = 3')
else if (operand.type == 'op' && !isLower && precedes(op.name, operand.name) <= 0) {
const { args } = operand
operand.args = [
{ type: 'op', name, args: [ t, args[0] ] },
args[1]
]
operand.args = [{ ...op, args: [t, args[0]] }, args[1]]
return operand
} else {
return { type: 'op', name, args: [ t, operand ] }
}
// parseExpr('a + b * c')
else if (operand.type !== 'op' && t.type == 'op' && precedes(op.name, t.name) < 0) {
t.args[1] = { ...op, args: [t.args[1], operand] }
return t
}
else {
return { ...op, args: [t, operand] }
}
}
else {
throw new Error(`Unexpected keyword ${name}`)
throw new Error(`Unexpected token ${op.name}`)
}

@@ -298,7 +334,10 @@ }

if (node) {
if (UNARY_OPERATORS.includes(node.name)) {
node.args[0] = expr()
} else if (MODIFIERS.includes(node.name)) {
// check arguments length to differentiate unary minus and binary minus
if (UNARY_OPERATORS.includes(node.name) && node.args.length == 0) {
node = unary(node)
}
else if (MODIFIERS.includes(node.name)) {
node.args[0] = token()
} else {
}
else {
node = operator(node)

@@ -312,2 +351,5 @@ }

}
else if (chr == '*') {
node = wildcard()
}
else {

@@ -367,2 +409,2 @@ node = token()

module.exports = { parseExpr, parseExprList }
module.exports = { parseExpr, parseExprList, precedes }

@@ -9,3 +9,3 @@ 'use strict'

const SqlString = require('sqlstring')
const { parseExprList, parseExpr } = require('./expr')
const { parseExprList, parseExpr, precedes } = require('./expr')

@@ -36,8 +36,11 @@ const OPERATOR_MAP = {

function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
/**
* 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

@@ -48,3 +51,3 @@ * @param {...*} values

function parseConditions(conditions, ...values) {
if (typeof conditions == 'object') {
if (isPlainObject(conditions)) {
return parseObjectConditions(conditions)

@@ -72,2 +75,46 @@ }

/**
* Check if object condition is an operator condition, such as `{ $gte: 100, $lt: 200 }`.
* @param {Object} condition
* @returns {boolean}
*/
function isOperatorCondition(condition) {
return isPlainObject(condition) &&
Object.keys(condition).length > 0 &&
Object.keys(condition).every($op => OPERATOR_MAP.hasOwnProperty($op))
}
/**
* parse operator condition into expression ast
* @example
* parseOperatorCondition('id', { $gt: 0, $lt: 999999 })
* // => { type: 'op', name: 'and', args: [ ... ]}
* @param {string} name
* @param {Object} condition
* @returns {Object}
*/
function parseOperatorCondition(name, condition) {
let node
for (const $op in condition) {
const op = OPERATOR_MAP[$op]
const args = [ parseExpr(name) ]
const val = condition[$op]
if (op == 'between' || op == 'not between') {
args.push(parseObjectValue(val[0]), parseObjectValue(val[1]))
} else {
args.push(parseObjectValue(val))
}
if (node) {
node = { type: 'op', name: 'and', args: [node, { type: 'op', name: op, args } ] }
} else {
node = { type: 'op', name: op, args }
}
}
return node
}
/**
* parse conditions in MongoDB style, which is quite polular in ORMs for JavaScript. See {@link module:lib/spell~OPERATOR_MAP} for supported `$op`s.

@@ -93,14 +140,4 @@ * @example

}
else if (value != null && typeof value == 'object' && !Array.isArray(value) && Object.keys(value).length == 1) {
for (const $op in value) {
const op = OPERATOR_MAP[$op]
const args = [ parseExpr(name) ]
const val = value[$op]
if (op == 'between' || op == 'not between') {
args.push(parseObjectValue(val[0]), parseObjectValue(val[1]))
} else {
args.push(parseObjectValue(val))
}
result.push({ type: 'op', name: op, args })
}
else if (isOperatorCondition(value)) {
result.push(parseOperatorCondition(name, value))
}

@@ -141,3 +178,2 @@ else {

}
token.value = model.unalias(value)
})

@@ -159,5 +195,5 @@ }

if (name in Model.schema) {
sets[Model.unalias(name)] = Model.uncast(obj[name], Model.schema[name].type)
sets[name] = Model.uncast(obj[name], Model.schema[name].type)
} else {
throw new Error(`Invalid attribute ${name}`)
throw new Error(`Undefined attribute "${name}"`)
}

@@ -207,7 +243,10 @@ }

const { value, qualifiers } = ast
const Model = findModel(spell, qualifiers)
const column = Model.unalias(value)
const { pool } = spell.Model
if (qualifiers && qualifiers.length > 0) {
return `${qualifiers.map(pool.escapeId).join('.')}.${pool.escapeId(value)}`
return `${qualifiers.map(pool.escapeId).join('.')}.${pool.escapeId(column)}`
} else {
return pool.escapeId(value)
return pool.escapeId(column)
}

@@ -286,3 +325,3 @@ }

const params = args.map(arg => {
return isLogicalOp(ast) && isLogicalOp(arg)
return isLogicalOp(ast) && isLogicalOp(arg) && precedes(name, arg.name) <= 0
? `(${formatExpr(spell, arg)})`

@@ -298,2 +337,5 @@ : formatExpr(spell, arg)

}
else if ('!~-'.includes(name) && params.length == 1) {
return `${name} ${params[0]}`
}
else if (args[1].type == 'literal' && args[1].value == null) {

@@ -506,3 +548,3 @@ if (['=', '!='].includes(name)) {

if (selection[qualifier].length > 0 && spell.dispatchable) {
selection[qualifier].push({ type: 'id', qualifiers: [qualifier], value: relation.Model.primaryColumn })
selection[qualifier].push({ type: 'id', qualifiers: [qualifier], value: relation.Model.primaryKey })
}

@@ -537,3 +579,3 @@ walkExpr(relation.on, ({ type, qualifiers, value }) => {

subspell.orders.unshift([{ type, value }, order])
if (Model.columns.includes(value)) token.qualifiers = [baseName]
if (Model.attributes.includes(value)) token.qualifiers = [baseName]
}

@@ -582,3 +624,3 @@ }

if (havingConditions.length > 0) {
if (!spell.derivable && havingConditions.length > 0) {
havingConditions.reduce(collectLiteral, values)

@@ -598,6 +640,6 @@ chunks.push(`HAVING ${formatConditions(spell, havingConditions)}`)

const { whereConditions } = spell
const { shardingKey, shardingColumn, table } = spell.Model
const { shardingKey, table } = spell.Model
if (shardingKey && !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingColumn }))) {
throw new Error(`Sharding key ${shardingKey} of table ${table} is required.`)
if (shardingKey && !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
throw new Error(`Sharding key ${table}.${shardingKey} is required.`)
}

@@ -616,7 +658,7 @@

const { Model, whereConditions } = spell
const { pool, shardingKey, shardingColumn } = Model
const { pool, shardingKey } = Model
const table = pool.escapeId(Model.table)
if (shardingKey && !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingColumn }))) {
throw new Error(`Sharding key ${shardingKey} of table ${Model.table} is required.`)
if (shardingKey && !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} is required.`)
}

@@ -642,3 +684,3 @@

.map(condition => {
return isLogicalOp(condition) && condition.name == 'or'
return isLogicalOp(condition) && condition.name == 'or' && conditions.length > 1
? `(${formatExpr(spell, condition)})`

@@ -657,6 +699,6 @@ : formatExpr(spell, condition)

const { pool, shardingKey } = Model
const columns = Object.keys(sets).map(pool.escapeId)
const columns = Object.keys(sets).map(column => pool.escapeId(Model.unalias(column)))
if (shardingKey && sets[shardingKey] == null) {
throw new Error(`Sharding key "${shardingKey}" of table "${Model.table}" is required.`)
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL.`)
}

@@ -676,12 +718,17 @@

const { Model, sets, whereConditions } = spell
const { pool, shardingKey, shardingColumn } = Model
const { pool, shardingKey } = Model
const values = []
const assigns = []
if (shardingKey && (sets[shardingKey] == null || !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingColumn })))) {
throw new Error(`Sharding key "${shardingKey}" of table "${Model.table}" is required.`)
if (shardingKey) {
if (sets.hasOwnProperty(shardingKey) && sets[shardingKey] == null) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL`)
}
if (!whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} is required.`)
}
}
for (const column in sets) {
assigns.push(`${pool.escapeId(column)} = ?`)
assigns.push(`${pool.escapeId(Model.unalias(column))} = ?`)
values.push(sets[column])

@@ -698,15 +745,30 @@ }

/**
* Taking advantage of MySQL's `on duplicate key update`, though knex does not support this because it has got several databases to be compliant of. PostgreSQL has got proper `upsert`. Maybe we can file a PR to knex someday.
*
* References:
* - http://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html
* - https://github.com/tgriesser/knex/issues/701
* upsert
* @param {Spell} spell
*/
function formatUpsert(spell) {
const { Model, sets } = spell
const { pool, shardingKey } = Model
if (shardingKey && sets[shardingKey] == null) {
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL`)
}
switch (pool.Leoric_type) {
case 'mysql':
return formatMysqlUpsert(spell)
default:
return formatDefaultUpsert(spell)
}
}
/**
* INSERT ... ON CONFLICT ... UPDATE SET
* - https://www.postgresql.org/docs/9.5/static/sql-insert.html
* - https://www.sqlite.org/lang_UPSERT.html
*
* @param {Spell} spell
*/
function formatUpsert(spell) {
function formatDefaultUpsert(spell) {
const { Model, sets } = spell
const { pool, shardingKey } = Model
const { pool, primaryColumn } = Model
const insert = formatInsert(spell)

@@ -716,16 +778,39 @@ const values = insert.values

if (shardingKey && sets[shardingKey] == null) {
throw new Error(`Sharding key "${shardingKey}" of table "${Model.table}" is required`)
for (const column in sets) {
assigns.push(`${pool.escapeId(Model.unalias(column))} = ?`)
values.push(sets[column])
}
return {
sql: `${insert.sql} ON CONFLICT (${pool.escapeId(primaryColumn)}) DO UPDATE SET ${assigns.join(', ')}`,
values
}
}
/**
* INSERT ... ON DUPLICATE KEY UPDATE
* - http://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html
* @param {Spell} spell
*/
function formatMysqlUpsert(spell) {
const { Model, sets } = spell
const { pool, primaryKey, primaryColumn } = Model
const insert = formatInsert(spell)
const assigns = []
const values = insert.values
// Make sure the correct LAST_INSERT_ID is returned.
// - https://stackoverflow.com/questions/778534/mysql-on-duplicate-key-last-insert-id
assigns.push(`${pool.escapeId(primaryColumn)} = LAST_INSERT_ID(${pool.escapeId(primaryColumn)})`)
for (const column in sets) {
assigns.push(`${pool.escapeId(column)} = ?`)
values.push(sets[column])
if (column !== primaryKey) {
assigns.push(`${pool.escapeId(Model.unalias(column))} = ?`)
values.push(sets[column])
}
}
return {
sql: pool.Leoric_type === 'mysql'
? `${insert.sql} ON DUPLICATE KEY UPDATE ${assigns.join(', ')}`
: `${insert.sql} ON CONFLICT (${pool.escapeId(Model.primaryColumn)}) DO UPDATE SET ${assigns.join(', ')}`,
values: values
sql: `${insert.sql} ON DUPLICATE KEY UPDATE ${assigns.join(', ')}`,
values
}

@@ -835,4 +920,4 @@ }

if (columns && !columns.includes(RefModel.primaryColumn)) {
columns.push(parseExpr(`${refName}.${RefModel.primaryColumn}`))
if (columns && !columns.includes(RefModel.primaryKey)) {
columns.push(parseExpr(`${refName}.${RefModel.primaryKey}`))
}

@@ -873,3 +958,3 @@ spell.joins[refName] = {

const Model = qualifier && qualifier != spell.Model.aliasName
? spell.joins[qualifier].Model
? (spell.joins.hasOwnProperty(qualifier) ? spell.joins[qualifier].Model : null)
: spell.Model

@@ -880,11 +965,2 @@ if (!Model) throw new Error(`Unabled to find model ${qualifiers}`)

function unalias(spell, expr) {
return walkExpr(expr, token => {
if (token.type == 'id') {
const Model = findModel(spell, token.qualifiers)
token.value = Model.unalias(token.value)
}
})
}
/**

@@ -901,4 +977,3 @@ * If Model supports soft delete, and deletedAt isn't specified in whereConditions yet, and the table isn't a subquery, append a default where({ deletedAt: null }).

walkExpr(condition, ({ type, value }) => {
if (type != 'id') return
if (value == 'deletedAt' || value == schema.deletedAt.column) {
if (type == 'id' && value == 'deletedAt') {
found = true

@@ -976,3 +1051,7 @@ }

}
return this.groups.length == 0
if (this.groups.length > 0) return false
if (this.table.value instanceof Spell && Object.keys(this.table.value.joins).length > 0) {
return false
}
return true
}

@@ -1021,9 +1100,7 @@

get first() {
const spell = this.order(this.Model.primaryKey)
return spell.$limit(1).$get(0)
return this.order(this.Model.primaryKey).$get(0)
}
get last() {
const spell = this.order(this.Model.primaryKey, 'desc')
return spell.$limit(1).$get(0)
return this.order(this.Model.primaryKey, 'desc').$get(0)
}

@@ -1053,3 +1130,3 @@

/**
* Get the instance from collection by index. Helper method for {@link Bone#first} and {@link Bone#last}.
* Get nth record.
* @param {number} index

@@ -1060,5 +1137,7 @@ * @returns {Bone}

const { factory, Model } = this
this.$limit(1)
if (index > 0) this.$offset(index)
this.factory = spell => {
return factory(spell).then(results => {
const result = results[index]
const result = results[0]
return result instanceof Model ? result : null

@@ -1170,6 +1249,19 @@ })

$where(conditions, ...values) {
this.whereConditions.push(...parseConditions(conditions, ...values).map(unalias.bind(null, this)))
this.whereConditions.push(...parseConditions(conditions, ...values))
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
}
/**

@@ -1193,3 +1285,3 @@ * Set GROUP BY attributes. `select_expr` with `AS` is supported, hence following expressions have the same effect:

for (const name of names) {
const token = unalias(this, parseExpr(name))
const token = parseExpr(name)
if (token.type == 'alias') {

@@ -1206,2 +1298,3 @@ columns.push(token)

/**
* Set the ORDER of the query
* @example

@@ -1212,19 +1305,19 @@ * .order('title')

* @param {string|Object} name
* @param {string} order
* @param {string} direction
* @returns {Spell}
*/
$order(name, order) {
if (typeof name == 'object') {
$order(name, direction) {
if (isPlainObject(name)) {
for (const prop in name) {
this.$order(prop, name[prop] || 'asc')
this.$order(prop, name[prop])
}
}
else if (!order) {
[name, order] = name.split(/\s+/)
this.$order(name, order || 'asc')
else if (name.includes(' ')) {
[name, direction] = name.split(/\s+/)
this.$order(name, direction)
}
else {
this.orders.push([
unalias(this, parseExpr(name)),
order && order.toLowerCase() == 'desc' ? 'desc' : 'asc'
parseExpr(name),
direction && direction.toLowerCase() == 'desc' ? 'desc' : 'asc'
])

@@ -1260,2 +1353,3 @@ }

/**
* Set the HAVING conditions, which usually appears in GROUP queries only.
* @example

@@ -1270,4 +1364,4 @@ * .having('average between ? and ?', 10, 20)

*/
$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

@@ -1284,3 +1378,3 @@ // https://stackoverflow.com/questions/32730296/referring-to-a-select-aggregate-column-alias-in-the-having-clause-in-postgres

}
this.havingConditions.push(unalias(this, condition))
this.havingConditions.push(condition)
}

@@ -1290,3 +1384,17 @@ 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

@@ -1302,3 +1410,3 @@ * .with('attachment')

for (const qualifier of qualifiers) {
if (typeof qualifier == 'object') {
if (isPlainObject(qualifier)) {
for (const key in qualifier) {

@@ -1311,6 +1419,2 @@ joinRelation(this, this.Model, this.Model.aliasName, key, qualifier[key])

}
for (const qualifier in this.joins) {
const relation = this.joins[qualifier]
relation.on = unalias(this, relation.on)
}
return this

@@ -1320,2 +1424,3 @@ }

/**
* LEFT JOIN arbitrary models with specified ON conditions.
* @example

@@ -1340,4 +1445,3 @@ * .join(User, 'users.id = posts.authorId')

}
joins[qualifier] = { Model }
joins[qualifier].on = unalias(this, parseConditions(onConditions, ...values)[0])
joins[qualifier] = { Model, on: parseConditions(onConditions, ...values)[0] }
return this

@@ -1372,3 +1476,3 @@ }

*/
batch(size = 1000) {
async * batch(size = 1000) {
const limit = parseInt(size, 10)

@@ -1378,25 +1482,16 @@ if (!(limit > 0)) throw new Error(`Invalid batch limit ${size}`)

const spell = this.limit(limit)
const { factory } = spell
let queue
let i = 0
const next = () => {
if (!queue) queue = factory(spell)
return queue.then(results => {
if (results.length == 0) return Promise.resolve({ done: true })
if (results[i]) {
return {
done: results.length < limit && i == results.length - 1,
value: results[i++]
}
} else {
queue = null
i = 0
spell.$offset(spell.skip + limit)
return next()
}
})
let results = await spell
while (results.length > 0) {
for (const result of results) {
yield result
}
results = await spell.$offset(spell.skip + limit)
}
return { next }
}
/**
* Format spell into `{ sql, values }`.
* @returns {Object}
*/
format() {

@@ -1403,0 +1498,0 @@ for (const scope of this.scopes) scope.call(this)

{
"name": "leoric",
"version": "0.4.1",
"version": "0.4.2",
"description": "JavaScript Object-relational mapping alchemy",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"jsdoc": "rm -rf docs/api && jsdoc -c .jsdoc.json -d docs/api -t node_modules/@cara/minami",
"pretest": "./test/prepare.sh",
"test": "DEBUG=leoric mocha --exit test/test.*.js",
"coveralls": "nyc mocha --exit test/test.*.js && nyc report --reporter=text-lcov | coveralls"
"test": "for test in $(ls test/test.*.js); do DEBUG=leoric mocha --exit --timeout 5000 ${test} || exit $?; done",
"coveralls": "for test in $(ls test/test.*.js); do nyc --no-clean mocha --exit --timeout 5000 ${test} || exit $?; done && nyc report --reporter=text-lcov | coveralls"
},

@@ -12,0 +13,0 @@ "repository": {

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc