Comparing version 0.1.0 to 0.1.1
@@ -113,3 +113,3 @@ 'use strict' | ||
for (let name in this.constructor.schema) { | ||
if (typeof this[name] !== 'undefined') { | ||
if (this[name] != null) { | ||
obj[name] = this[name] | ||
@@ -120,3 +120,3 @@ } | ||
for (let name in this) { | ||
if (this.hasOwnProperty(name) && typeof this[name] !== 'undefined') { | ||
if (this.hasOwnProperty(name) && this[name] != null) { | ||
obj[name] = this[name] | ||
@@ -178,3 +178,3 @@ } | ||
const Model = this.constructor | ||
const { schema, pool, primaryKey, table } = Model | ||
const { schema, pool, primaryKey } = Model | ||
@@ -208,3 +208,3 @@ if (schema.createdAt && !this.createdAt) this.createdAt = new Date() | ||
return spell.upsert(data) | ||
return spell.$upsert(data) | ||
} | ||
@@ -235,3 +235,3 @@ | ||
const Model = this.constructor | ||
const { pool, primaryKey, schema, table } = Model | ||
const { pool, primaryKey, schema } = Model | ||
const data = {} | ||
@@ -259,3 +259,3 @@ | ||
return spell.insert(data) | ||
return spell.$insert(data) | ||
} | ||
@@ -497,3 +497,2 @@ | ||
static relate(name, opts) { | ||
@@ -503,24 +502,3 @@ if (name in this.relations) { | ||
} | ||
const { foreignKey, hasMany, belongsTo, through, includes, select, where } = opts | ||
const Model = this.reflectClass(opts.Model) | ||
const relation = { | ||
Model, | ||
through, | ||
includes, | ||
foreignKey, | ||
hasMany, | ||
belongsTo, | ||
where | ||
} | ||
if (select) { | ||
const attributes = new Set(Array.from(Model.attributes).filter(Spell.parseSelect(select))) | ||
for (const name of ['createdAt', 'updatedAt']) { | ||
if (name in Model.schema) attributes.add(name) | ||
} | ||
relation.attributes = attributes | ||
} | ||
this.relations[name] = relation | ||
this.relations[name] = { ...opts, Model: this.reflectClass(opts.Model) } | ||
} | ||
@@ -593,9 +571,9 @@ | ||
if (typeof conditions == 'object' && values.length == 1 && typeof values[0] == 'object') { | ||
spell.where(conditions) | ||
spell.$where(conditions) | ||
for (const method of ['order', 'limit', 'offset', 'select']) { | ||
const value = values[0][method] | ||
if (value != null) spell[method](value) | ||
if (value != null) spell[`$${method}`](value) | ||
} | ||
} else if (conditions) { | ||
spell.where(conditions, ...values) | ||
spell.$where(conditions, ...values) | ||
} | ||
@@ -607,9 +585,13 @@ | ||
static findOne(conditions, ...values) { | ||
const spell = this.find(conditions, ...values).limit(1) | ||
spell.promise = spell.promise.then(results => { | ||
const spell = this.find(conditions, ...values).$limit(1) | ||
spell.onresolve = results => { | ||
return results.length > 0 && results[0] instanceof this ? results[0] : null | ||
}) | ||
} | ||
return spell | ||
} | ||
static include(...names) { | ||
return this.find().$with(...names) | ||
} | ||
static where(conditions, ...values) { | ||
@@ -620,15 +602,15 @@ return this.find(conditions, ...values) | ||
static select(...attributes) { | ||
return this.find().select(...attributes) | ||
return this.find().$select(...attributes) | ||
} | ||
static group(...names) { | ||
return this.find().group(...names) | ||
return this.find().$group(...names) | ||
} | ||
static join(Model, conditions, ...values) { | ||
return this.find().join(Model, conditions, ...values) | ||
return this.find().$join(Model, conditions, ...values) | ||
} | ||
static count(conditions, ...values) { | ||
return this.find(conditions, ...values).count() | ||
return this.find(conditions, ...values).$count() | ||
} | ||
@@ -653,7 +635,7 @@ | ||
spell.where(conditions) | ||
spell.$where(conditions) | ||
if (this.schema.updatedAt && !values.updatedAt && !values.deletedAt) { | ||
values.updatedAt = new Date() | ||
} | ||
spell.update(values) | ||
spell.$update(values) | ||
@@ -677,3 +659,3 @@ return spell | ||
}) | ||
return spell.where(conditions).delete() | ||
return spell.$where(conditions).$delete() | ||
} | ||
@@ -680,0 +662,0 @@ else if (this.schema.deletedAt) { |
329
lib/spell.js
@@ -71,3 +71,2 @@ 'use strict' | ||
if (value instanceof Spell) { | ||
value.cancelled = true | ||
result.push({ | ||
@@ -79,3 +78,3 @@ type: 'op', | ||
} | ||
else if (value != null && typeof value == 'object') { | ||
else if (value != null && typeof value == 'object' && !Array.isArray(value) && Object.keys(value).length == 1) { | ||
for (const $op in value) { | ||
@@ -105,2 +104,14 @@ const op = OPERATOR_MAP[$op] | ||
function parseSelect(select) { | ||
const type = typeof select | ||
if (type == 'function') return select | ||
if (type == 'string') select = select.split(/\s+/) | ||
if (Array.isArray(select)) { | ||
return select.includes.bind(select) | ||
} else { | ||
throw new Error(`Invalid select ${select}`) | ||
} | ||
} | ||
function formatOrders(spell) { | ||
@@ -195,3 +206,3 @@ const formatOrder = ([name, order]) => { | ||
function formatSelectWithoutJoin(spell) { | ||
const { Model, attributes, whereConditions, groups, havingConditions, orders, limit, offset } = spell | ||
const { Model, attributes, whereConditions, groups, havingConditions, orders, rowCount, skip } = spell | ||
const chunks = [] | ||
@@ -230,4 +241,4 @@ | ||
if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(spell).join(', ')}`) | ||
if (limit > 0) chunks.push(`LIMIT ${limit}`) | ||
if (offset > 0) chunks.push(`OFFSET ${offset}`) | ||
if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`) | ||
if (skip > 0) chunks.push(`OFFSET ${skip}`) | ||
@@ -246,11 +257,16 @@ return chunks.join(' ') | ||
function formatSelectWithJoin(spell) { | ||
const { Model, attributes, whereConditions, groups, orders, limit, offset, joins } = spell | ||
const { Model, attributes, whereConditions, groups, orders, rowCount, skip, joins } = spell | ||
const subspell = spell.dup | ||
const baseName = Model.alias | ||
const allAttrs = { [baseName]: [] } | ||
const selection = { [baseName]: new Set() } | ||
function appendColumn(node) { | ||
if (node.type == 'id' && node.qualifiers && node.qualifiers.length > 0) { | ||
const { qualifiers: [qualifier], value } = node | ||
if (Array.isArray(allAttrs[qualifier]) && (qualifier == baseName || qualifier in joins)) { | ||
allAttrs[qualifier].push(value) | ||
for (const name of attributes) { | ||
const { type, qualifiers, value } = parseExpr(name) | ||
if (type == 'id') { | ||
if (qualifiers && qualifiers[0] != baseName) { | ||
const [qualifier] = qualifiers | ||
if (!selection[qualifier]) selection[qualifier] = new Set() | ||
selection[qualifier].add(value) | ||
} else { | ||
selection[baseName].add(value) | ||
} | ||
@@ -261,21 +277,38 @@ } | ||
for (const qualifier in joins) { | ||
const { on, attributes: select } = joins[qualifier] | ||
if (select) { | ||
allAttrs[qualifier] = select | ||
} else { | ||
allAttrs[qualifier] = '*' | ||
const relation = joins[qualifier] | ||
if (!selection[qualifier]) { | ||
selection[qualifier] = relation.attributes || new Set() | ||
} | ||
walkExpr(on, appendColumn) | ||
if (selection[qualifier].size > 0) selection[qualifier].add(relation.Model.primaryKey) | ||
walkExpr(relation.on, ({ type, qualifiers, value }) => { | ||
if (type == 'id' && qualifiers && qualifiers[0] == baseName) { | ||
const attrs = selection[qualifiers[0]] | ||
if (attrs.size > 0) attrs.add(value) | ||
} | ||
}) | ||
} | ||
if (spell.attributes.size > 0) { | ||
for (const name of allAttrs[baseName]) spell.attributes.add(name) | ||
spell.attributes.add(Model.primaryKey) | ||
subspell.attributes = selection[baseName] | ||
selection[baseName] = new Set() | ||
subspell.whereConditions = [] | ||
for (let i = whereConditions.length - 1; i >= 0; i--) { | ||
const condition = whereConditions[i] | ||
let internal = true | ||
walkExpr(condition, ({ type, qualifiers }) => { | ||
if (type == 'id' && qualifiers && qualifiers[0] != baseName) { | ||
internal = false | ||
} | ||
}) | ||
if (internal) { | ||
subspell.whereConditions.push(condition) | ||
whereConditions.splice(i, 1) | ||
} | ||
} | ||
allAttrs[baseName] = '*' | ||
const columns = [] | ||
for (const qualifier in allAttrs) { | ||
for (const qualifier in selection) { | ||
const { Model: RefModel } = qualifier == baseName ? Model : joins[qualifier] | ||
const attrs = allAttrs[qualifier] | ||
if (attrs instanceof Set) { | ||
const attrs = selection[qualifier] | ||
if (attrs.size > 0) { | ||
for (const name of attrs) { | ||
@@ -293,10 +326,9 @@ columns.push(`${escapeId(qualifier)}.${escapeId(RefModel.unalias(name))}`) | ||
if (attributes.size > 0 || whereConditions.length > 0 || groups.size > 0 || orders.length > 0 || offset > 0 || limit > 0) { | ||
chunks.push(`FROM (${formatSelectWithoutJoin(spell)})`) | ||
subspell.orders = [] | ||
if (subspell.attributes.size > 0 || subspell.whereConditions.length > 0 || skip > 0 || rowCount > 0) { | ||
chunks.push(`FROM (${formatSelectWithoutJoin(subspell)}) AS ${escapeId(baseName)}`) | ||
} else { | ||
chunks.push(`FROM ${escapeId(Model.table)}`) | ||
chunks.push(`FROM ${escapeId(Model.table)} AS ${escapeId(baseName)}`) | ||
} | ||
chunks.push(`AS ${escapeId(baseName)}`) | ||
for (const qualifier in joins) { | ||
@@ -319,2 +351,5 @@ const { Model: RefModel, on } = joins[qualifier] | ||
if (whereConditions.length > 0) chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`) | ||
if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(spell).join(', ')}`) | ||
return chunks.join(' ') | ||
@@ -378,6 +413,3 @@ } | ||
/** | ||
* 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. | ||
* 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. | ||
* | ||
@@ -448,5 +480,8 @@ * References: | ||
} else { | ||
if (relation.select && !relation.attributes) { | ||
relation.attributes = new Set(Array.from(RefModel.attributes).filter(parseSelect(relation.select))) | ||
} | ||
const { select, throughRelation } = opts | ||
const attributes = select | ||
? new Set(Array.from(RefModel.attributes).filter(spell.constructor.parseSelect(select))) | ||
? new Set(Array.from(RefModel.attributes).filter(parseSelect(select))) | ||
: relation.attributes | ||
@@ -465,53 +500,47 @@ | ||
function isCalculation(func) { | ||
return ['count', 'average', 'minimun', 'maximum', 'sum'].includes(func ) | ||
} | ||
/** | ||
* 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 }). | ||
*/ | ||
function Spell_deletedAtIsNull() { | ||
const { Model, table, whereConditions } = this | ||
for (const token of whereConditions) { | ||
const { type, value } = token.args[0] | ||
if (type == 'id' && value == 'deletedAt') return this | ||
} | ||
if (table.type == 'id' && Model.schema.deletedAt) { | ||
return this.$where({ deletedAt: null }) | ||
} | ||
return this | ||
} | ||
class Spell { | ||
constructor(Model, fn) { | ||
this.promise = new Promise(resolve => { | ||
setImmediate(() => { | ||
if (!this.cancelled) resolve(fn(this.toSqlString())) | ||
}) | ||
}) | ||
constructor(Model, factory, opts) { | ||
this.Model = Model | ||
this.command = 'select' | ||
this.attributes = new Set() | ||
this.table = parseExpr(Model.table) | ||
this.whereConditions = [] | ||
this.groups = new Set() | ||
this.orders = [] | ||
this.havingConditions = [] | ||
this.joins = {} | ||
this.subqueryIndex = 0 | ||
this.scopes = [ | ||
/** | ||
* 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 }). | ||
*/ | ||
function Spell_deletedAtIsNull() { | ||
const { Model, table, whereConditions } = this | ||
for (const ast of whereConditions) { | ||
const { type, value } = ast.args[0] | ||
if (type == 'id' && value == 'deletedAt') return | ||
} | ||
if (table.type == 'id' && Model.schema.deletedAt) { | ||
this.where({ deletedAt: null }) | ||
} | ||
} | ||
] | ||
this.factory = factory | ||
this.triggered = false | ||
Object.assign(this, { | ||
command: 'select', | ||
attributes: new Set(), | ||
table: parseExpr(Model.table), | ||
whereConditions: [], | ||
groups: new Set(), | ||
orders: [], | ||
havingConditions: [], | ||
joins: {}, | ||
scopes: [ Spell_deletedAtIsNull ], | ||
subqueryIndex: 0 | ||
}, opts) | ||
} | ||
static parseSelect(select) { | ||
const type = typeof select | ||
if (type == 'function') return select | ||
if (type == 'string') select = select.split(/\s+/) | ||
if (Array.isArray(select)) { | ||
return select.includes.bind(select) | ||
} else { | ||
throw new Error(`Invalid select ${select}`) | ||
} | ||
} | ||
get dispatchable() { | ||
const { attributes, Model } = this | ||
const { attributes } = this | ||
for (const name of attributes) { | ||
if (!(name in Model.schema)) return false | ||
const { type, name: func } = parseExpr(name) | ||
if (type == 'func' && isCalculation(func)) return false | ||
// currently `foo as bar` is parsed as `{ type: 'op', name: 'as', args: ['foo', 'bar'] }` | ||
if (type == 'op') return false | ||
} | ||
@@ -522,10 +551,48 @@ return this.groups.size == 0 | ||
get unscoped() { | ||
this.scopes = [] | ||
return this | ||
const spell = this.dup | ||
spell.scopes = [] | ||
return spell | ||
} | ||
get dup() { | ||
return new Spell(this.Model, this.factory, { | ||
command: this.command, | ||
attributes: new Set(Array.from(this.attributes)), | ||
values: this.values, | ||
table: this.table, | ||
whereConditions: [].concat(this.whereConditions), | ||
groups: new Set(Array.from(this.groups)), | ||
orders: [].concat(this.orders), | ||
havingConditions: [].concat(this.havingConditions), | ||
joins: this.joins, | ||
skip: this.skip, | ||
rowCount: this.rowCount, | ||
scopes: [].concat(this.scopes), | ||
triggered: false, | ||
onresolve: this.onresolve | ||
}) | ||
} | ||
/** | ||
* Fake this as a thenable object. | ||
* @param {Function} resolve | ||
* @param {Function} reject | ||
*/ | ||
then(resolve, reject) { | ||
return this.promise.then(resolve, reject) | ||
// `Model.findOne()` needs to edit the resolved results before returning it to user. Hence a intermediate resolver called `onresolve` is added. It gets copied when the spell is duplicated too. | ||
let promise = new Promise(resolve => { | ||
setImmediate(() => { | ||
const { factory } = this | ||
resolve(factory(this.toSqlString())) | ||
}) | ||
}) | ||
const { onresolve } = this | ||
if (onresolve) promise = promise.then(onresolve) | ||
return promise.then(resolve, reject) | ||
} | ||
/** | ||
* Same as above | ||
* @param {Function} reject | ||
*/ | ||
catch(reject) { | ||
@@ -535,3 +602,3 @@ return this.promise.catch(reject) | ||
insert(values) { | ||
$insert(values) { | ||
this.values = values | ||
@@ -542,3 +609,3 @@ this.command = 'insert' | ||
upsert(values) { | ||
$upsert(values) { | ||
this.values = values | ||
@@ -549,3 +616,3 @@ this.command = 'upsert' | ||
select(...names) { | ||
$select(...names) { | ||
if (names.length == 1) names = names[0].split(/\s+/) | ||
@@ -556,3 +623,3 @@ for (const name of names) this.attributes.add(name) | ||
update(values) { | ||
$update(values) { | ||
this.values = values | ||
@@ -563,3 +630,3 @@ this.command = 'update' | ||
delete() { | ||
$delete() { | ||
this.command = 'delete' | ||
@@ -569,13 +636,10 @@ return this | ||
from(table) { | ||
if (table instanceof Spell) { | ||
table.cancelled = true | ||
this.table = { type: 'array', value: table } | ||
} else { | ||
this.table = parseExpr(table) | ||
} | ||
$from(table) { | ||
this.table = table instanceof Spell | ||
? { type: 'array', value: table } | ||
: parseExpr(table) | ||
return this | ||
} | ||
where(conditions, ...values) { | ||
$where(conditions, ...values) { | ||
this.whereConditions.push(...parseConditions(conditions, ...values)) | ||
@@ -585,3 +649,3 @@ return this | ||
group(...names) { | ||
$group(...names) { | ||
for (const name of names) this.groups.add(name) | ||
@@ -591,26 +655,34 @@ return this | ||
order(name, order) { | ||
if (!order) { | ||
$order(name, order) { | ||
if (typeof name == 'object') { | ||
for (const prop in name) { | ||
this.$order(prop, name[prop] || 'asc') | ||
} | ||
} | ||
else if (!order) { | ||
[name, order] = name.split(/\s+/) | ||
this.$order(name, order || 'asc') | ||
} | ||
if (!isIdentifier(name)) throw new Error(`Invalid order attribute ${name}`) | ||
this.orders.push([name, order.toLowerCase() == 'desc' ? 'desc' : 'asc']) | ||
else { | ||
this.orders.push([name, order && order.toLowerCase() == 'desc' ? 'desc' : 'asc']) | ||
} | ||
return this | ||
} | ||
offset(offset) { | ||
offset = +offset | ||
if (Number.isNaN(offset)) throw new Error(`Invalid offset ${offset}`) | ||
this.offset = offset | ||
$offset(skip) { | ||
skip = +skip | ||
if (Number.isNaN(skip)) throw new Error(`Invalid offset ${skip}`) | ||
this.skip = skip | ||
return this | ||
} | ||
limit(limit) { | ||
limit = +limit | ||
if (Number.isNaN(limit)) throw new Error(`Invalid limit ${limit}`) | ||
this.limit = limit | ||
$limit(rowCount) { | ||
rowCount = +rowCount | ||
if (Number.isNaN(rowCount)) throw new Error(`Invalid limit ${rowCount}`) | ||
this.rowCount = rowCount | ||
return this | ||
} | ||
having(conditions, values) { | ||
$having(conditions, values) { | ||
this.havingConditions.push(...parseConditions(conditions, values)) | ||
@@ -620,3 +692,3 @@ return this | ||
count(name = '*') { | ||
$count(name = '*') { | ||
name = isIdentifier(name) ? name : '*' | ||
@@ -633,3 +705,3 @@ this.attributes = new Set([`count(${name}) as count`]) | ||
*/ | ||
with(...names) { | ||
$with(...names) { | ||
for (const name of names) { | ||
@@ -651,3 +723,3 @@ if (typeof name == 'object') { | ||
* Post.join(User, 'users.id = posts.authorId') | ||
* Post.join(TagMap, 'tagMaps.targetId = posts.id and tagMaps.targetType = ?', 0) | ||
* Post.join(TagMap, 'tagMaps.targetId = posts.id and tagMaps.targetType = 0') | ||
* | ||
@@ -658,15 +730,13 @@ * @param {Model} Model | ||
*/ | ||
join(Model, onConditions, ...values) { | ||
$join(Model, onConditions, ...values) { | ||
if (typeof Model == 'string') { | ||
return this.with(...arguments) | ||
return this.$with(...arguments) | ||
} | ||
const qualifier = Model.alias | ||
const { joins } = this | ||
const prop = Model.alias | ||
if (prop in this.joins) { | ||
throw new Error(`Invalid join target. ${prop} already defined.`) | ||
if (qualifier in joins) { | ||
throw new Error(`Invalid join target. ${qualifier} already defined.`) | ||
} | ||
this.joins[prop] = { | ||
Model, | ||
on: parseConditions(onConditions, ...values)[0] | ||
} | ||
joins[qualifier] = { Model, on: parseConditions(onConditions, ...values)[0] } | ||
@@ -678,2 +748,3 @@ return this | ||
for (const scope of this.scopes) scope.call(this) | ||
switch (this.command) { | ||
@@ -696,3 +767,2 @@ case 'insert': | ||
toString() { | ||
this.cancelled = true | ||
return this.toSqlString() | ||
@@ -702,2 +772,15 @@ } | ||
for (const method of Object.getOwnPropertyNames(Spell.prototype)) { | ||
if (method[0] == '$') { | ||
const descriptor = Object.getOwnPropertyDescriptor(Spell.prototype, method) | ||
Object.defineProperty(Spell.prototype, method.slice(1), Object.assign({}, descriptor, { | ||
value: function() { | ||
const spell = this.dup | ||
spell[method](...arguments) | ||
return spell | ||
} | ||
})) | ||
} | ||
} | ||
module.exports = Spell |
{ | ||
"name": "leoric", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Object-relational mapping for Node.js", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -7,2 +7,2 @@ # Leoric | ||
Leoric is an object-relational mapping for Node.js, which is heavily influenced by Active Record. | ||
Leoric is an object-relational mapping for Node.js, which is heavily influenced by Active Record. See the [documentation](http://cyj.me/leoric) for detail. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
48981
11
1563