Skip to content

Commit

Permalink
New: mysql2 and sqlite3 support (fixes #1)
Browse files Browse the repository at this point in the history
  • Loading branch information
cyjake committed Jan 17, 2018
1 parent c13e4bf commit 16fe0d4
Show file tree
Hide file tree
Showing 22 changed files with 1,659 additions and 1,403 deletions.
8 changes: 7 additions & 1 deletion History.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
0.3.0 / 2018-01-??
==================

* New: SQLite support with a [forked sqlite3](https://github.com/dotnil/node-sqlite3)
* New: mysql2 support (which is trivial since both mysql and mysql2 share the same API)
* Refactor: Spell now formats SQL with the literals separated, which gets escaped by the corresponding client itself later on.

0.2.0 / 2018-01-03
==================

* Breaking: renamed from Leoric to Jorma


0.1.8 / 2017-12-31
==================

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2017 Chen Yangjian
Copyright 2018 Chen Yangjian

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Expand Down
6 changes: 5 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ class Post extends Bone {

async function() {
// connect models to database
await connect({ host: 'example.com', models: [Post], /* among other options */ })
await connect({
client: 'mysql',
host: 'example.com', /* among other connection options */
models: [Post]
})

// CRUD
await Post.create({ title: 'New Post' })
Expand Down
129 changes: 68 additions & 61 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,8 @@ const Bone = require('./lib/bone')
const Collection = require('./lib/collection')

const fs = require('fs')
const mysql = require('mysql')
const path = require('path')

/**
* Create a connection pool
* @param {Object} opts
* @param {string} opts.host
* @param {string} opts.port
* @param {string} opts.user
* @param {string} opts.password
* @param {string} opts.appName - In some RDMS, appName is used as the name of the database
* @param {string} opts.db
* @param {string} opts.connectionLimit
* @returns {Pool} the connection pool
*/
function createPool({ host, port, user, password, appName, db, connectionLimit }) {
if (!host) {
throw new Error('Please config sql server first.')
}

const pool = mysql.createPool({
connectionLimit,
host,
port,
user,
password,
// some RDMS use appName to locate the database instead of the actual db, though the table_schema stored in infomation_schema.columns is still the latter one.
database: appName || db
})

return pool
}

function readdir(path, opts = {}) {
return new Promise((resolve, reject) => {
fs.readdir(path, opts, function(err, entries) {
Expand All @@ -54,61 +23,99 @@ function readdir(path, opts = {}) {
* Fetch column infomations from schema database
*
* - https://dev.mysql.com/doc/refman/5.7/en/columns-table.html
* - https://www.postgresql.org/docs/current/static/infoschema-columns.html
*
* @param {Pool} pool
* @param {string} db
* @param {Array} tables
* @param {string} database
* @param {Object} schema
*/
function columnInfo(pool, db, tables) {
return new Promise((resolve, reject) => {
async function schemaInfo(pool, database, tables) {
const [results] = await new Promise((resolve, reject) => {
pool.query(
'SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = ? AND table_name in (?)',
[db, tables],
[database, tables],
function(err, results, fields) {
if (err) reject(err)
else resolve([results, fields])
}
)
})
const schema = {}
for (const result of results) {
const { table_name, column_name, data_type, is_nullable, column_default } = result
const columns = schema[table_name] || (schema[table_name] = [])
columns.push({
name: column_name,
type: data_type,
isNullable: is_nullable,
default: column_default
})
}
return schema
}

async function tableInfo(pool, tables) {
const queries = tables.map(table => {
return new Promise((resolve, reject) => {
pool.query(`PRAGMA table_info(${pool.escapeId(table)})`, (err, rows, fields) => {
if (err) reject(err)
else resolve([rows, fields])
})
})
})
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) {
switch (name) {
case 'mysql':
case 'mysql2':
return require('./lib/clients/mysql')
case 'pg':
return require('./lib/clients/pg')
case 'sqlite3':
return require('./lib/clients/sqlite')
default:
throw new Error(`Unsupported database ${name}`)
}
}

/**
* Connect models to database. Need to provide both the settings of the connection and the models, or the path of the models, to connect.
* @alias module:index.connect
* @param {Object} opts - connect options
* @param {string} opts.path - path of the models
* @param {string} opts.models - an array of models
* @param {Object} opts
* @param {string} opts.client - client name
* @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 Jorma_connect(opts) {
if (Bone.pool) return

const models = opts.path
? (await readdir(opts.path)).map(entry => require(path.join(opts.path, entry)))
opts = Object.assign({ client: 'mysql', database: opts.db }, opts)
const { client, database } = opts
const pool = findClient(client)(opts)
const models = typeof opts.models == 'string'
? (await readdir(opts.models)).map(entry => require(path.join(opts.models, entry)))
: opts.models

if (!(models && models.length > 0)) {
throw new Error('Unable to find any models')
}

const pool = createPool(opts)
if (!(models && models.length > 0)) throw new Error('Unable to find any models')

Bone.pool = pool
Collection.pool = pool
const [results] = await columnInfo(pool, opts.db, models.map(m => m.table))
const schema = {}

for (const result of results) {
const { table_name, column_name, data_type, is_nullable, column_default } = result
const columns = schema[table_name] || (schema[table_name] = [])
columns.push({
name: column_name,
type: data_type,
isNullable: is_nullable,
default: column_default
})
}

const query = client.includes('sqlite')
? tableInfo(pool, models.map(model => model.table))
: schemaInfo(pool, database, models.map(model => model.table))
const schema = await query
for (const Model of models) {
Model.describeTable(schema[Model.table])
}
Expand Down
51 changes: 33 additions & 18 deletions lib/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const deepEqual = require('deep-equal')
const util = require('util')
const pluralize = require('pluralize')
const SqlString = require('sqlstring')
const debug = require('debug')(require('../package.json').name)

const Collection = require('./collection')
Expand Down Expand Up @@ -296,13 +297,21 @@ class Bone {
return Promise.resolve()
}

// sqlite3 doesn't have upsert. fallback to select/update instead.
if (pool.client == 'sqlite3') {
return Model.find(this.id).first
.then(record => {
return record ? Model.update({ id: this.id }, data) : Model.create(data)
})
}

// About LAST_INSERT_ID()
// - http://dev.mysql.com/doc/refman/5.7/en/information-functions.html#function_last-insert-id
const spell = new Spell(Model, spell => {
const sql = spell.toSqlString()
debug(sql)
const { sql, values } = spell.format()
debug(SqlString.format(sql, values))
return new Promise((resolve, reject) => {
pool.query(sql, (err, results) => {
pool.query(sql, values, (err, results) => {
const { affectedRows, insertId } = results
this[primaryKey] = insertId
this.syncRaw()
Expand Down Expand Up @@ -360,10 +369,10 @@ class Bone {
}

const spell = new Spell(Model, spell => {
const sql = spell.toSqlString()
debug(sql)
const { sql, values } = spell.format()
debug(SqlString.format(sql, values))
return new Promise((resolve, reject) => {
pool.query(sql, (err, results, fields) => {
pool.query(sql, values, (err, results, fields) => {
if (err) return reject(err)
this[primaryKey] = results.insertId
this.syncRaw()
Expand Down Expand Up @@ -635,6 +644,8 @@ class Bone {
switch (type) {
case JSON:
return value ? JSON.parse(value) : null
case Date:
return value instanceof Date ? value : new Date(value)
default:
return value
}
Expand Down Expand Up @@ -744,11 +755,15 @@ class Bone {
results.push(this.instantiate(Object.values(entry)[0]))
}
} else {
const { aliasName, primaryColumn, primaryKey } = this
const { aliasName, table, primaryColumn, primaryKey } = this
let current
for (const entry of entries) {
if (!current || current[primaryKey] != entry[aliasName][primaryColumn]) {
current = this.instantiate(entry[aliasName])
// If SQL contains subqueries, such as `SELECT * FROM (SELECT * FROM foo) AS bar`,
// the table name of the columns in SQLite is the original table name instead of the alias.
// Hence we need to fallback to original table name here.
const main = entry[aliasName] || entry[table]
if (!current || current[primaryKey] != main[primaryColumn]) {
current = this.instantiate(main)
results.push(current)
}
for (const qualifier in spell.joins) {
Expand Down Expand Up @@ -850,10 +865,10 @@ class Bone {
static find(conditions, ...values) {
const { pool } = this
const spell = new Spell(this, spell => {
const sql = spell.toSqlString()
debug(sql)
const { sql, values } = spell.format()
debug(SqlString.format(sql, values))
return new Promise((resolve, reject) => {
pool.query({ sql, nestTables: true }, (err, results, fields) => {
pool.query({ sql, nestTables: true }, values, (err, results, fields) => {
if (err) {
reject(err)
} else {
Expand Down Expand Up @@ -939,10 +954,10 @@ class Bone {
static update(conditions, values) {
const { pool } = this
const spell = new Spell(this, spell => {
const sql = spell.toSqlString()
debug(sql)
const { sql, values } = spell.format()
debug(SqlString.format(sql, values))
return new Promise((resolve, reject) => {
pool.query(sql, (err, results) => {
pool.query(sql, values, (err, results) => {
if (err) reject(err)
else resolve(results.affectedRows)
})
Expand Down Expand Up @@ -977,10 +992,10 @@ class Bone {

if (forceDelete) {
const spell = new Spell(this, spell => {
const sql = spell.toSqlString()
debug(sql)
const { sql, values } = spell.format()
debug(SqlString.format(sql, values))
return new Promise((resolve, reject) => {
pool.query(sql, (err, results) => {
pool.query(sql, values, (err, results) => {
if (err) reject(err)
else resolve(results.affectedRows)
})
Expand Down
31 changes: 31 additions & 0 deletions lib/clients/mysql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict'

/**
* Create a connection pool
* @param {Object} opts
* @param {string} opts.client
* @param {string} opts.host
* @param {string} opts.port
* @param {string} opts.user
* @param {string} opts.password
* @param {string} opts.appName - In some RDMS, appName is used as the name of the database
* @param {string} opts.database
* @param {string} opts.connectionLimit
* @returns {Pool} the connection pool
*/
function Jorma_mysql({ client, host, port, user, password, appName, database, connectionLimit }) {
if (client != 'mysql' && client != 'mysql2') {
throw new Error(`Unsupported mysql client ${client}`)
}
return require(client).createPool({
connectionLimit,
host,
port,
user,
password,
// some RDMS use appName to locate the database instead of the actual db, though the table_schema stored in infomation_schema.columns is still the latter one.
database: appName || database
})
}

module.exports = Jorma_mysql
Loading

0 comments on commit 16fe0d4

Please sign in to comment.