streamsql
Version:
Streaming SQL ORM
509 lines (407 loc) • 12.5 kB
JavaScript
const Stream = require('stream')
const util = require('util')
const Promise = require('bluebird')
const map = require('map-stream')
const xtend = require('xtend')
const create = require('./lib/create')
const hasCallback = require('./lib/has-callback')
const callbackResolver = require('./lib/callback-resolver')
const fmt = util.format.bind(util)
const dbProto = {}
const tableProto = {}
dbProto.close = function close(callback) {
return this.driver.close(this.connection, callback)
}
dbProto.table = function table(name, definition) {
if (definition)
return this.registerTable.apply(this, arguments)
const tableDefinition = this.tables[name]
if (!tableDefinition)
throw new Error(fmt('No table registered with the name `%s`', name))
return tableDefinition
}
dbProto.registerTable = function registerTable(name, def) {
const fields = def.fields || []
const primaryKey = def.primaryKey || 'id'
if (fields.indexOf(primaryKey) === -1)
fields.unshift(primaryKey)
if (!def.hasOwnProperty('constructor')) {
def.constructor = function createRow (data) {
return create(table.row, data)
}
}
const table = create(tableProto, {
table: def.tableName || name,
primaryKey: primaryKey,
fields: fields,
methods: def.methods || {},
row: def.methods || {},
constructor: def.constructor,
relationships: def.relationships || {},
db: this,
})
this.tables[name] = table
return table
}
tableProto.put = function put(row, opts, callback) {
var resolver = Promise.defer()
opts = opts || {}
if (hasCallback(arguments)) {
if (typeof opts == 'function') {
callback = opts
opts = {}
}
resolver = callbackResolver(callback)
}
const table = this.table
const primaryKey = this.primaryKey
var uniqueKey
if (opts.uniqueKey) {
uniqueKey = typeof opts.uniqueKey === 'string'
? [opts.uniqueKey]
: opts.uniqueKey
}
const driver = this.db.driver
const insertSql = driver.insertSql(table, row)
const tryUpdate = primaryKey in row || uniqueKey
const meta = { row: row, sql: null, insertId: null }
const query = this.db.query(insertSql, function (err, result) {
if (err) {
if (tryUpdate && driver.putShouldUpdate(err, opts)) {
const keys = uniqueKey ? uniqueKey : [primaryKey]
const cond = keys.reduce(function (cond, key) {
cond[key] = row[key]
return cond
}, {})
return this.update(row, cond, resolver.callback)
}
return resolver.reject(err)
}
meta.sql = insertSql
meta.insertId = result.insertId
return resolver.resolve(meta)
}.bind(this))
return resolver.promise
}
tableProto.update = function update(row, conditions, callback) {
var resolver = Promise.defer()
conditions = conditions || {}
if (hasCallback(arguments)) {
if (typeof conditions == 'function') {
callback = conditions
conditions = {}
}
resolver = callbackResolver(callback)
}
const table = this.table
const driver = this.db.driver
const updateSql = driver.updateSql(table, row, conditions)
const query = this.db.query(updateSql, handleResult)
const meta = {
row: row,
sql: updateSql,
affectedRows: null
}
function handleResult(err, result) {
if (err) { return resolver.reject(err) }
meta.affectedRows = result.affectedRows
return resolver.resolve(meta)
}
return resolver.promise
}
tableProto.get = function get(cnd, opts, callback) {
var resolver = Promise.defer()
cnd = cnd || {}
opts = opts || {}
if (hasCallback(arguments)) {
if (typeof opts == 'function') {
callback = opts
opts = {}
}
if (typeof cnd == 'function') {
callback = cnd
cnd = {}
opts = {}
}
resolver = callbackResolver(callback)
}
const self = this;
const RowClass = self.constructor
const driver = self.db.driver
const table = self.table
const tableCache = self.db.tables
const relationships = buildRelationships(self, opts.relationships, opts.relationshipsDepth)
var error = {}
const selectSql = driver.selectSql({
db: self.db,
table: table,
tables: tableCache,
relationships: relationships,
fields: self.fields,
conditions: cnd,
limit: opts.limit,
include: opts.include,
exclude: opts.exclude,
page: opts.page,
order: opts.sort || opts.order || opts.orderBy,
})
if (typeof selectSql == 'object' && selectSql.name == 'RangeError') {
error = selectSql
return setImmediate(resolver.reject.bind(null, error))
}
self.db.query(selectSql, opts.single ? singleRow : manyRows)
const hydrOpts = {
table: table,
relationships: relationships,
tableCache: tableCache,
}
function singleRow(err, rows) {
if (err) return resolver.reject(err)
if (!rows.length) return resolver.resolve()
const singleton = rows[0]
if (!singleton)
return resolver.resolve()
driver.hydrateRow(singleton, hydrOpts, function (err, result) {
if (err) return resolver.reject(err)
return resolver.resolve(new RowClass(result))
})
}
function manyRows(err, rows) {
if (err) return resolver.reject(err)
driver.hydrateRows(rows, hydrOpts, function (err, rows) {
if (err) return resolver.reject(err)
const hydratedRows = rows.map(function (row) {
return new RowClass(row)
})
if (!opts.includeTotal)
return resolver.resolve(hydratedRows)
const countSql = ''
+ 'SELECT COUNT(*) AS `total` FROM $table '
+ driver.whereSql(table, cnd)
self.getOne(countSql, function (err, data) {
if (err) return resolver.reject(err)
data.rows = hydratedRows
return resolver.resolve(data)
})
})
}
if (opts.debug)
console.error(selectSql)
return resolver.promise
}
tableProto.getAll = function getAll(opts, callback) {
return this.get({}, opts, callback)
}
tableProto.getOne = function getOne(cnd, opts, callback) {
if (typeof opts == 'function') {
callback = opts
opts = {}
}
const singularOpts = { limit: 1, single: true }
return this.get(cnd, xtend(opts, singularOpts), callback)
}
tableProto.del = function del(cnd, opts, callback) {
var resolver = Promise.defer()
if (hasCallback(arguments)) {
if (typeof opts == 'function') {
callback = opts
opts = null
}
resolver = callbackResolver(callback)
}
opts = opts || {}
const query = this.db.query
const driver = this.db.driver
const table = this.table
const deleteSql = driver.deleteSql({
table: table,
conditions: cnd,
limit: opts.limit
})
if (opts.debug)
console.error(deleteSql)
// the callback for query generally has an arity of 3, we only want an
// arity of 2, so we have to do this little dance.
query(deleteSql, function (err, success) {
resolver.callback(err, success)
})
return resolver.promise
}
tableProto.createReadStream = function createReadStream(conditions, opts) {
opts = opts || {}
const conn = this.db.connection
const driver = this.db.driver
const fields = this.fields
const table = this.table
const relationships = buildRelationships(this, opts.relationships, opts.relationshipsDepth)
const selectSql = driver.selectSql({
db: this.db,
table: table,
tables: this.db.tables,
fields: fields,
conditions: conditions,
limit: opts.limit,
page: opts.page,
relationships: relationships,
include: opts.include,
exclude: opts.exclude,
order: opts.order || opts.orderBy,
})
const tableCache = this.db.tables
const queryStream = this.db.queryStream(selectSql, {
rowPrototype: this.row,
relationships: relationships,
tableCache: this.db.tables,
table: table,
})
if (opts.debug)
console.error(selectSql)
return queryStream
}
tableProto.createKeyStream = function createKeyStream(conditions, opts) {
const primaryKey = this.primaryKey
opts = xtend(opts, {
orderBy: opts.orderBy || primaryKey,
include: [ primaryKey ]
})
const keyStream = this.createReadStream(conditions, opts)
.pipe(map(function (row, next) {
return next(null, row[primaryKey])
}))
return keyStream
}
tableProto.createWriteStream = function createWriteStream(opts) {
opts = opts || {}
const ignoreDupes = opts.ignoreDupes
const conn = this.db.connection
const table = this.table
const stream = new Stream()
const emit = stream.emit.bind(stream)
const put = this.put.bind(this)
var waiting = 0
var ending = false
function done() {
['finish', 'close', 'end'].forEach(emit)
}
function drain() {
waiting -= 1
emit('drain')
if (ending && waiting <= 0)
return done()
}
stream.readable = true
stream.writable = true
stream.write = function write(row, callback) {
waiting += 1
put(row, function handleResult(err, meta) {
if (err) {
if (ignoreDupes) {
emit('dupe', row)
return drain()
}
if (callback) { callback(err) }
emit('error', err, meta)
return drain()
}
emit('meta', meta)
emit('data', row)
if (callback && typeof callback == 'function')
callback(null, meta)
return drain()
})
return false;
}
stream.end = function end(row, callback) {
stream.readable = false
if (typeof row == 'function') {
callback = row
row = null
}
if (callback)
stream.on('end', callback)
if (row) {
stream.write(row)
return stream.end()
}
if (waiting > 0)
ending = true
else
done()
}
return stream
}
const base = module.exports = {
connect: function connect(options, callback) {
const driver = getDriver(options.driver)
const connection = driver.connect(options, callback)
return create(dbProto, {
connection: connection,
query: driver.getQueryFn(connection),
queryStream: driver.getStreamFn(connection),
tables: {},
driver: driver,
})
}
}
function getDriver(name) {
const drivers = require('./lib/drivers')
const prototype = require('./lib/drivers/prototype')
const implementation = drivers[name || 'mysql']()
return create(prototype, implementation)
}
function buildRelationships(instance, relationships, depth) {
if (typeof relationships == 'boolean' &&
relationships === true) {
// Use all relationships if `relationships === true`
relationships = instance.relationships
} else if (typeof relationships == 'number') {
// Use all relationships
depth = relationships
relationships = instance.relationships
} else if (relationships instanceof Array) {
// Use subset of relationships if array of keys given
relationships = relationships.reduce(function (r, relation) {
if (instance.relationships.hasOwnProperty(relation))
r[relation] = instance.relationships[relation]
return r
}, {})
} else if (instance.relationships.hasOwnProperty(relationships)) {
// Use single relationship if key given
var relation = relationships
relationships = {}
relationships[relation] = instance.relationships[relation]
} else {
// Use given relationships, or empty if not provided
relationships = relationships || {}
}
return _buildRelationships(instance.table, relationships, depth, instance.db.tables)
}
function _buildRelationships (table, base, depth, tableCache, history) {
if (depth === true)
depth = Infinity
depth = depth === Infinity || depth < 0 ? Infinity : parseInt(depth, 10)
if (isNaN(depth) || !depth)
return base
history = history || []
return Object.keys(base).reduce(function (relationships, relation) {
const relationship = base[relation]
const key = [table, relationship.foreign.table].join(':')
if (history.indexOf(key) > -1)
return relationships
history.push(key)
relationship.foreign.as = relationship.foreign.as || relation
const foreignTable = tableCache[relationship.foreign.table];
if (foreignTable && (depth - 1))
relationship.relationships =
_buildRelationships(
relationship.foreign.table,
foreignTable.relationships,
depth - 1,
tableCache,
history
)
relationships = relationships || {}
relationships[relation] = relationship
return relationships
}, undefined)
}