Skip to content

Commit

Permalink
New: Model.get(index)
Browse files Browse the repository at this point in the history
- refactored Model.first, Model.last with the new Model.get(index)
- fixed several bugs found during applying this module in production,
  test cases added too.
- added test cases about date functions, group('func() as column') isn't
  supported yet, save it to next release.
  • Loading branch information
cyjake committed Dec 21, 2017
1 parent 1c5707a commit ec3eba9
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 49 deletions.
8 changes: 8 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
0.1.6 / 2017-12-21
==================

* New: proper `.first`, `.last`, `.all`, and `.get(index)`
* Fix: accept `Date`, `boolean`, and `Set` values
* Fix: `Model.unscoped`
* Fix: `Model.remove({}, true)` should be unscoped

0.1.5 / 2017-12-20
==================

Expand Down
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function() {
| `Post.update({ id: 42 }, { title: 'Skeleton King' })` | `UPDATE posts SET title = 'Skeleton King' WHERE id = 42` |
| `Post.remove({ id: 42 })` | `DELETE FROM posts WHERE id = 42` |

A more detailed syntax table may be found at the [documentation](http://cyj.me/leoric) site.
A more detailed syntax table may be found at the [documentation](http://cyj.me/leoric/#syntax-table) site.

## Migrations

Expand Down
31 changes: 20 additions & 11 deletions lib/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,11 +588,19 @@ class Bone {
}

static get first() {
return this.findOne().$order(this.primaryKey)
return this.find().first
}

static get last() {
return this.findOne().$order(this.primaryKey, 'desc')
return this.find().last
}

static get unscoped() {
return this.find().unscoped
}

static get(index) {
return this.find().$get(index)
}

static find(conditions, ...values) {
Expand Down Expand Up @@ -633,28 +641,29 @@ class Bone {

static findOne(conditions, ...values) {
const spell = this.find(conditions, ...values).$limit(1)
spell.onresolve = results => {
return results.length > 0 && results[0] instanceof this ? results[0] : null
}
return spell
return spell.$get(0)
}

static include(...names) {
return this.find().$with(...names)
}

static where(conditions, ...values) {
return this.find(conditions, ...values)
}

static select(...attributes) {
return this.find().$select(...attributes)
}

static where(conditions, ...values) {
return this.find(conditions, ...values)
}

static group(...names) {
return this.find().$group(...names)
}

static order(name, order) {
return this.find().$order(name, order)
}

static join(Model, conditions, ...values) {
return this.find().$join(Model, conditions, ...values)
}
Expand Down Expand Up @@ -721,7 +730,7 @@ class Bone {
})
})
})
return spell.$where(conditions).$delete()
return spell.unscoped.$where(conditions).$delete()
}
else if (this.schema.deletedAt) {
return this.update(conditions, { deletedAt: new Date() })
Expand Down
12 changes: 12 additions & 0 deletions lib/expr.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,24 @@ function parseExpr(str, ...values) {
else if (Array.isArray(value)) {
return { type: 'array', value }
}
else if (value instanceof Date) {
return { type: 'date', value }
}
else if (value instanceof Set) {
return { type: 'array', value: Array.from(value) }
}
else if (typeof value == 'number') {
return { type: 'number', value }
}
else if (typeof value == 'string') {
return { type: 'string', value }
}
else if (typeof value == 'boolean') {
return { type: 'boolean', value: value }
}
else if (typeof value.toSqlString == 'function') {
return { type: 'subquery', value }
}
else {
throw new Error(`Unexpected value ${value} at ${valueIndex - 1}`)
}
Expand Down
122 changes: 94 additions & 28 deletions lib/spell.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ function isIdentifier(name) {
return /^[0-9a-z$_.]+$/i.test(name)
}

/**
* Allows two types of params:
*
* parseConditions({ foo: { $op: value } })
* parseConditions('foo = ?', value)
*
* @param {(string|Object)} conditions
* @param {...*} values
*/
function parseConditions(conditions, ...values) {
if (typeof conditions == 'object') {
return parseObjectConditions(conditions)
Expand All @@ -43,24 +52,29 @@ function parseConditions(conditions, ...values) {
}
}

/**
* A wrapper of parseExpr to help parsing values in object conditions.
* @param {*} value
*/
function parseObjectValue(value) {
if (value == null) {
return { type: 'null' }
}
else if (Array.isArray(value)) {
return { type: 'array', value }
}
else if (typeof value == 'string') {
return { type: 'string', value }
try {
return parseExpr('?', value)
}
else if (typeof value == 'number') {
return { type: 'number', value }
}
else {
catch (err) {
throw new Error(`Unexpected object value ${value}`)
}
}

/**
* parse conditions in MongoDB style, which is quite polular in ORMs for JavaScript, e.g.
*
* { foo: null }
* { foo: { $gt: new Date(2012, 4, 15) } }
* { foo: { $between: [1, 10] } }
*
* See OPERATOR_MAP for supported `$op`s.
* @param {Object} conditions
*/
function parseObjectConditions(conditions) {
const result = []

Expand All @@ -71,7 +85,7 @@ function parseObjectConditions(conditions) {
result.push({
type: 'op',
name: 'in',
args: [ parseExpr(name), { type: 'array', value } ]
args: [ parseExpr(name), { type: 'subquery', value } ]
})
}
else if (value != null && typeof value == 'object' && !Array.isArray(value) && Object.keys(value).length == 1) {
Expand Down Expand Up @@ -99,6 +113,15 @@ function parseObjectConditions(conditions) {
return result
}

/**
* A helper method that translates select to filter function from following types:
*
* name => ['foo', 'bar'].includes(name) // as is
* 'foo bar' //
* ['foo', 'bar'] // as the arrow function above
*
* @param {(function|string|string[])} select
*/
function parseSelect(select) {
const type = typeof select

Expand All @@ -111,6 +134,11 @@ function parseSelect(select) {
}
}

/**
* Translate key-value pairs of attributes into key-value pairs of columns. Get ready for the SET part when generating SQL.
* @param {Spell} spell
* @param {Object} obj - key-value pairs of attributes
*/
function parseSet(spell, obj) {
const { Model } = spell
const sets = {}
Expand All @@ -121,14 +149,29 @@ function parseSet(spell, obj) {
return sets
}

function formatOrders(spell) {
/**
* Format orders into ORDER BY clause in SQL
* @param {Object[]} orders
*/
function formatOrders(orders) {
const formatOrder = ([token, order]) => {
const column = formatColumn(token)
return order == 'desc' ? `${column} DESC` : column
}
return spell.orders.map(formatOrder)
return orders.map(formatOrder)
}

/**
* Format token into identifiers/functions/etc. in SQL
*
* formatColumn({ type: 'id', value: 'title' })
* // => `title`
*
* formatColumn({ type: 'func', name: 'year', args: [ { type: 'id', value: 'createdAt' } ] })
* // => YEAR(`createdAt`)
*
* @param {(string|Object)} token
*/
function formatColumn(token) {
if (typeof token == 'string') token = parseExpr(token)

Expand All @@ -154,10 +197,14 @@ function formatExpr(ast) {
switch (type) {
case 'string':
case 'number':
case 'date':
case 'boolean':
case 'null':
return escape(value)
case 'subquery':
return `(${value.toSqlString()})`
case 'array':
return `(${value.toSqlString ? value.toSqlString() : escape(value)})`
return `(${escape(value)})`
case 'wildcard':
return '*'
case 'id':
Expand Down Expand Up @@ -252,7 +299,7 @@ function formatSelectWithoutJoin(spell) {
chunks.push(`HAVING ${formatConditions(havingConditions)}`)
}

if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(spell).join(', ')}`)
if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(orders).join(', ')}`)
if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`)
if (skip > 0) chunks.push(`OFFSET ${skip}`)

Expand Down Expand Up @@ -400,7 +447,7 @@ function formatSelectWithJoin(spell) {
}
}

if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(spell).join(', ')}`)
if (orders.length > 0) chunks.push(`ORDER BY ${formatOrders(orders).join(', ')}`)

return chunks.join(' ')
}
Expand Down Expand Up @@ -575,7 +622,6 @@ class Spell {
constructor(Model, factory, opts) {
this.Model = Model
this.factory = factory
this.triggered = false
Object.assign(this, {
command: 'select',
columns: [],
Expand Down Expand Up @@ -637,6 +683,20 @@ class Spell {
return spell
}

get all() {
return this
}

get first() {
const spell = this.order(this.Model.primaryKey)
return spell.$get(0)
}

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

get dup() {
return new Spell(this.Model, this.factory, {
command: this.command,
Expand All @@ -650,27 +710,33 @@ class Spell {
joins: this.joins,
skip: this.skip,
rowCount: this.rowCount,
scopes: [...this.scopes],
triggered: false,
onresolve: this.onresolve
scopes: [...this.scopes]
})
}

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

/**
* Fake this as a thenable object.
* @param {Function} resolve
* @param {Function} reject
*/
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 => {
this.promise = new Promise(resolve => {
setImmediate(() => {
resolve(this.factory.call(null, this))
})
})
const { onresolve } = this
if (onresolve) promise = promise.then(onresolve)
return promise.then(resolve, reject)
return this.promise.then(resolve, reject)
}

/**
Expand Down Expand Up @@ -714,7 +780,7 @@ class Spell {

$from(table) {
this.table = table instanceof Spell
? { type: 'array', value: table }
? { type: 'subquery', value: table }
: parseExpr(table)
return this
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "leoric",
"version": "0.1.5",
"version": "0.1.6",
"description": "Object-relational mapping for Node.js",
"main": "index.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions tests/dumpfile.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CREATE TABLE `articles` (
`extra` text,
`thumb` varchar(1000) DEFAULT NULL,
`author_id` bigint(20) unsigned DEFAULT NULL,
`is_private`tinyint(1) unsigned DEFAULT 0,
PRIMARY KEY (`id`)
);

Expand Down
Loading

0 comments on commit ec3eba9

Please sign in to comment.