├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lucas Barrena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # like-mysql 2 | 3 | Simple and intuitive ORM for MySQL 4 | 5 | ![](https://img.shields.io/npm/v/like-mysql.svg) ![](https://img.shields.io/npm/dt/like-mysql.svg) ![](https://img.shields.io/badge/tested_with-tape-e683ff.svg) ![](https://img.shields.io/github/license/LuKks/like-mysql.svg) 6 | 7 | ```javascript 8 | const mysql = require('like-mysql') 9 | 10 | // create a pool easily with good defaults 11 | const db = mysql('127.0.0.1:3306', 'root', 'secret', 'myapp') 12 | 13 | // wait until a connection is established 14 | await db.ready() 15 | 16 | // INSERT INTO `ips` (`addr`, `hits`) VALUES (?, ?) 17 | const id = await db.insert('ips', { addr: req.ip, hits: 0 }) 18 | 19 | // SELECT `addr`, `hits` FROM `ips` WHERE addr = ? 20 | const rows = await db.select('ips', ['addr', 'hits'], 'addr = ?', req.ip) 21 | 22 | // SELECT `addr`, `hits` FROM `ips` WHERE addr = ? LIMIT 1 23 | const row = await db.selectOne('ips', ['addr', 'hits'], 'addr = ?', req.ip) 24 | 25 | // SELECT EXISTS(SELECT 1 FROM `ips` WHERE addr = ? LIMIT 1) 26 | const exists = await db.exists('ips', 'addr = ?', req.ip) 27 | 28 | // SELECT COUNT(1) FROM `ips` WHERE addr = ? 29 | const count = await db.count('ips', 'addr = ?', req.ip) 30 | 31 | // UPDATE `ips` SET `hits` = ? WHERE addr = ? LIMIT 1 32 | await db.update('ips', { hits: 1 }, 'addr = ? LIMIT 1', req.ip) 33 | 34 | // UPDATE `ips` SET `hits` = hits + ? WHERE addr = ? 35 | await db.update('ips', [{ hits: 'hits + ?' }, 1], 'addr = ?', req.ip) 36 | 37 | // DELETE FROM `ips` WHERE addr = ? LIMIT 1 38 | await db.delete('ips', 'addr = ? LIMIT 1', req.ip) 39 | 40 | // getConnection, beginTransaction, callback, commit/rollback, release 41 | await db.transaction(async function (conn) { 42 | const id = await conn.insert('users', { username: 'lukks', ... }) 43 | await conn.insert('profiles', { owner: id, ... }) 44 | }) 45 | 46 | // execute 47 | const [res, fields] = await db.execute('SELECT * FROM `ips` WHERE `addr` = ?', [req.ip]) 48 | 49 | // query 50 | const [res, fields] = await db.query('SELECT * FROM `ips` WHERE `addr` = "8.8.8.8"') 51 | 52 | // end pool 53 | await db.end() 54 | ``` 55 | 56 | ## Install 57 | ``` 58 | npm i like-mysql 59 | ``` 60 | 61 | ## Description 62 | [node-mysql2](https://github.com/sidorares/node-mysql2) is used to create the MySQL pool.\ 63 | [like-sql](https://github.com/lukks/like-sql) is used to build the SQL queries.\ 64 | Operations are prepared statements made by `execute`.\ 65 | Promise version. All custom methods are also promised. 66 | 67 | Automatic `WHERE` when `find` argument doesn't start with:\ 68 | `ORDER BY`, `LIMIT` or `GROUP BY` 69 | 70 | ## Examples 71 | #### constructor 72 | ```javascript 73 | // host:port 74 | const db = mysql('127.0.0.1:3306', 'root', 'secret', 'mydb') 75 | 76 | // socketPath 77 | const db = mysql('/var/lib/mysql/mysql.sock', 'root', 'secret', 'mydb') 78 | ``` 79 | 80 | #### ready 81 | Wait for database started by docker-compose, etc. 82 | ```javascript 83 | // default timeout (15s) 84 | await db.ready() // will throw in case is not able to connect 85 | 86 | // custom timeout 87 | await db.ready(5000) 88 | ``` 89 | 90 | #### insert 91 | ```javascript 92 | // with autoincrement id: 93 | const insertId = await db.insert('ips', { addr: req.ip, hits: 0 }) 94 | console.log(insertId) // => 1336 95 | 96 | // otherwise it always returns zero: 97 | const insertId = await db.insert('config', { key: 'title', value: 'Database' }) 98 | console.log(insertId) // => 0 99 | ``` 100 | 101 | #### select 102 | ```javascript 103 | const rows = await db.select('ips', ['*'], 'addr = ?', req.ip) 104 | console.log(rows) // => [{ id: 2, addr: '8.8.4.4', hits: 2 }] 105 | 106 | const rows = await db.select('ips', ['addr', 'hits'], 'ORDER BY hits DESC') 107 | console.log(rows) // => [{ addr: '8.8.8.8', hits: 6 }, { addr: '8.8.4.4', hits: 2 }, ...] 108 | ``` 109 | 110 | #### selectOne 111 | ```javascript 112 | const row = await db.selectOne('ips', ['addr', 'hits'], 'addr = ?', req.ip) 113 | console.log(row) // => { addr: '8.8.4.4', hits: 2 } 114 | 115 | const row = await db.selectOne('ips', ['addr', 'hits'], 'addr = ?', '0.0.0.0') 116 | console.log(row) // => undefined 117 | ``` 118 | 119 | #### exists 120 | ```javascript 121 | const exists = await db.exists('ips', 'addr = ?', req.ip) 122 | console.log(exists) // => true 123 | ``` 124 | 125 | #### count 126 | ```javascript 127 | const total = await db.count('ips', 'addr = ?', req.ip) 128 | console.log(total) // => 2 129 | ``` 130 | 131 | #### update 132 | ```javascript 133 | const changedRows = await db.update('ips', { hits: 1 }, 'addr = ?', req.ip) 134 | console.log(changedRows) // => 1 135 | 136 | const changedRows = await db.update('ips', [{ hits: 'hits + ?' }, 1], 'addr = ?', req.ip) 137 | console.log(changedRows) // => 1 138 | ``` 139 | 140 | #### delete 141 | ```javascript 142 | const affectedRows = await db.delete('ips', 'addr = ?', req.ip) 143 | console.log(affectedRows) // => 1 144 | ``` 145 | 146 | #### transaction 147 | Normally with a pool you do something like: 148 | - `conn = pool.getConnection()` 149 | - `conn.beginTransaction()` 150 | - `conn.execute('INSERT INTO users (username, password) VALUES (?, ?)')` 151 | - `conn.execute('INSERT INTO profile (owner, name) VALUES (?, ?)')` 152 | - `conn.commit()` 153 | - `conn.release()` 154 | 155 | Also checking different catchs to release and/or rollback. 156 | 157 | This method simplifies all that and you just do the important part: 158 | ```javascript 159 | await db.transaction(async function (conn) { 160 | const id = await conn.insert('users', { username: 'lukks', ... }) 161 | await conn.insert('profiles', { owner: id, ... }) 162 | }) 163 | ``` 164 | 165 | You can also return a custom value: 166 | ```javascript 167 | const result = await db.transaction(async function (conn) { 168 | await conn.insert(...) 169 | return 'custom value' 170 | }) 171 | 172 | console.log(result) // => 'custom value' 173 | ``` 174 | 175 | #### end 176 | ```javascript 177 | await db.end() 178 | ``` 179 | 180 | ## Tests 181 | Start a database instance 182 | ``` 183 | docker run --rm -p 3305:3306 -e MYSQL_ROOT_USER=root -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 184 | ``` 185 | 186 | Run tests 187 | ``` 188 | npm test 189 | ``` 190 | 191 | Stop container and due --rm will be auto deleted 192 | ``` 193 | docker ps 194 | docker stop cc6 195 | ``` 196 | 197 | ## License 198 | Code released under the [MIT License](https://github.com/LuKks/like-mysql/blob/master/LICENSE). 199 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const SQL = require('like-sql') 2 | const mysql = require('mysql2/promise') 3 | // + support for sqlite? 4 | 5 | module.exports = function (hostname, user, password, database, opts = {}) { 6 | return new LikePool(hostname, user, password, database, opts) 7 | } 8 | 9 | class MySQL extends SQL { 10 | constructor (opts = {}) { 11 | super(opts) 12 | 13 | this.charset = opts.charset === undefined ? 'utf8mb4' : opts.charset 14 | this.collate = opts.collate === undefined ? 'utf8mb4_unicode_ci' : opts.collate 15 | this.engine = opts.engine === undefined ? 'InnoDB' : opts.engine 16 | } 17 | 18 | async _createDatabase (sql) { 19 | const [res] = await this.execute(sql) 20 | return res.warningStatus === 0 21 | } 22 | 23 | async _dropDatabase (sql) { 24 | const [res] = await this.execute(sql) 25 | return res.warningStatus === 0 26 | } 27 | 28 | async _createTable (sql) { 29 | const [res] = await this.execute(sql) 30 | return res.warningStatus === 0 31 | } 32 | 33 | async _dropTable (sql) { 34 | const [res] = await this.execute(sql) 35 | return res.warningStatus === 0 36 | } 37 | 38 | async _insert (sql, values) { 39 | const [res] = await this.execute(sql, values) 40 | return res.insertId 41 | } 42 | 43 | async _select (sql, values) { 44 | const [res] = await this.execute(sql, values) 45 | return res 46 | } 47 | 48 | async _selectOne (sql, values) { 49 | const [res] = await this.execute(sql, values) 50 | return res.length ? res[0] : undefined 51 | } 52 | 53 | async _exists (sql, values) { 54 | const [res, fields] = await this.execute(sql, values) 55 | return !!res[0][fields[0].name] 56 | } 57 | 58 | async _count (sql, values) { 59 | const [res, fields] = await this.execute(sql, values) 60 | return res[0][fields[0].name] 61 | } 62 | 63 | async _update (sql, values) { 64 | const [res] = await this.execute(sql, values) 65 | return res.changedRows 66 | } 67 | 68 | async _delete (sql, values) { 69 | const [res] = await this.execute(sql, values) 70 | return res.affectedRows 71 | } 72 | 73 | static parseHostname (host) { 74 | let port 75 | let socketPath 76 | const semicolon = host.indexOf(':') 77 | if (semicolon > -1) { // host:port 78 | port = host.substr(semicolon + 1) 79 | host = host.substr(0, semicolon) 80 | } else { // socket path 81 | socketPath = host 82 | host = '' 83 | } 84 | return { host, port, socketPath } 85 | } 86 | } 87 | 88 | class LikePool extends MySQL { 89 | constructor (hostname, user, password, database, opts = {}) { 90 | super(opts) 91 | 92 | const { host, port, socketPath } = MySQL.parseHostname(hostname) 93 | 94 | this.pool = mysql.createPool({ 95 | host: host || '127.0.0.1', 96 | port: port || 3306, 97 | socketPath: socketPath || '', 98 | user: user || 'root', 99 | password: password || '', 100 | database: database || '', 101 | charset: this.collate, // in createPool options, charset is collate value 102 | supportBigNumbers: typeof opts.supportBigNumbers === 'boolean' ? opts.supportBigNumbers : true, 103 | bigNumberStrings: typeof opts.bigNumberStrings === 'boolean' ? opts.bigNumberStrings : false, 104 | decimalNumbers: typeof opts.decimalNumbers === 'boolean' ? opts.decimalNumbers : true, 105 | connectionLimit: opts.connectionLimit || 20, 106 | waitForConnections: typeof opts.waitForConnections === 'boolean' ? opts.waitForConnections : true, 107 | ssl: opts.ssl 108 | }) 109 | } 110 | 111 | async getConnection () { 112 | const conn = await this.pool.getConnection() 113 | const db = new LikeConnection(conn) 114 | return db 115 | } 116 | 117 | async ready (timeout = 15000) { 118 | const started = Date.now() 119 | 120 | while (true) { 121 | try { 122 | const conn = await this.pool.getConnection() 123 | conn.release() 124 | break 125 | } catch (error) { 126 | if (error.code === 'ER_ACCESS_DENIED_ERROR') throw error 127 | if (error.code === 'ER_BAD_DB_ERROR') throw error 128 | if (error.message === 'No connections available.') throw error 129 | if (Date.now() - started > timeout) throw error 130 | 131 | await sleep(500) 132 | } 133 | } 134 | 135 | function sleep (ms) { 136 | return new Promise(resolve => setTimeout(resolve, ms)) 137 | } 138 | } 139 | 140 | query (sql, values) { 141 | return this.pool.query(sql, values) 142 | } 143 | 144 | execute (sql, values) { 145 | return this.pool.execute(sql, values) 146 | } 147 | 148 | end () { 149 | return this.pool.end() 150 | } 151 | 152 | async transaction (callback) { 153 | const conn = await this.getConnection() 154 | let output 155 | 156 | await releaseOnError(conn, conn.beginTransaction()) 157 | 158 | try { 159 | output = await callback(conn) 160 | await conn.commit() 161 | } catch (err) { 162 | await releaseOnError(conn, conn.rollback()) 163 | 164 | conn.release() 165 | throw err 166 | } 167 | 168 | conn.release() 169 | 170 | return output 171 | 172 | function releaseOnError (conn, promise) { 173 | return promise.catch(err => { 174 | conn.release() 175 | throw err 176 | }) 177 | } 178 | } 179 | } 180 | 181 | class LikeConnection extends MySQL { 182 | constructor (conn, opts = {}) { 183 | super(opts) 184 | 185 | this.connection = conn 186 | } 187 | 188 | query (sql, values) { 189 | return this.connection.query(sql, values) 190 | } 191 | 192 | execute (sql, values) { 193 | return this.connection.execute(sql, values) 194 | } 195 | 196 | end () { 197 | return this.connection.end() 198 | } 199 | 200 | destroy () { 201 | return this.connection.destroy() 202 | } 203 | 204 | beginTransaction () { 205 | return this.connection.beginTransaction() 206 | } 207 | 208 | commit () { 209 | return this.connection.commit() 210 | } 211 | 212 | rollback () { 213 | return this.connection.rollback() 214 | } 215 | 216 | release () { 217 | return this.connection.release() 218 | } 219 | } 220 | 221 | // mysqldump -h 127.0.0.1 --port=3307 -u root -p meli categories > categories.sql 222 | // db.dump() 223 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "like-mysql", 3 | "version": "2.0.4", 4 | "description": "Simple and intuitive ORM for MySQL", 5 | "main": "index.js", 6 | "dependencies": { 7 | "like-sql": "^0.1.0", 8 | "mysql2": "^2.3.3" 9 | }, 10 | "devDependencies": { 11 | "standard": "^16.0.4", 12 | "tape": "^5.5.0" 13 | }, 14 | "scripts": { 15 | "test": "standard && tape test.js", 16 | "tape": "tape test.js", 17 | "lint": "standard" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/LuKks/like-mysql.git" 22 | }, 23 | "author": "Lucas Barrena ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/LuKks/like-mysql/issues" 27 | }, 28 | "homepage": "https://github.com/LuKks/like-mysql", 29 | "keywords": [ 30 | "like", 31 | "mysql", 32 | "simple", 33 | "intuitive", 34 | "orm", 35 | "mysql2", 36 | "promise" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const mysql = require('./') 3 | 4 | const cfg = ['127.0.0.1:3305', 'root', 'secret', 'sys'] 5 | 6 | tape('able to connect', async function (t) { 7 | const db = mysql(...cfg) 8 | 9 | await db.ready() 10 | 11 | await db.end() 12 | }) 13 | 14 | tape('ready timeouts', async function (t) { 15 | const db = mysql('127.0.0.1:1234', cfg[1], cfg[2], cfg[3]) 16 | 17 | try { 18 | await db.ready(0) 19 | t.fail('ready with wrong host:port') 20 | } catch (error) { 21 | t.ok(error.code === 'ECONNREFUSED') 22 | } 23 | 24 | await db.end() 25 | }) 26 | 27 | tape('should not connect due unexisting database', async function (t) { 28 | const db = mysql(cfg[0], cfg[1], cfg[2], 'database-not-exists') 29 | 30 | try { 31 | await db.ready() 32 | t.fail('ready with unexisting database') 33 | } catch (error) { 34 | t.ok(error.code === 'ER_BAD_DB_ERROR') 35 | } 36 | 37 | await db.end() 38 | }) 39 | 40 | tape('should not connect due wrong authentication', async function (t) { 41 | const db = mysql(cfg[0], cfg[1], 'wrong-password', cfg[3]) 42 | 43 | try { 44 | await db.ready() 45 | t.fail('ready with wrong auth') 46 | } catch (error) { 47 | t.ok(error.code === 'ER_ACCESS_DENIED_ERROR') 48 | } 49 | 50 | await db.end() 51 | }) 52 | 53 | tape('limited connections', async function (t) { 54 | const db = mysql(...cfg, { connectionLimit: 1, waitForConnections: false }) 55 | 56 | await db.ready() 57 | 58 | const conn = await db.getConnection() 59 | 60 | try { 61 | await db.ready() 62 | t.fail('ready without any connection in the pool available') 63 | } catch (error) { 64 | t.ok(error.message === 'No connections available.') 65 | } 66 | 67 | conn.release() 68 | 69 | await db.end() 70 | }) 71 | 72 | tape('createDatabase() and dropDatabase()', async function (t) { 73 | const db = mysql(...cfg) 74 | 75 | await db.ready() 76 | 77 | t.ok(await db.createDatabase('forex')) 78 | t.notOk(await db.createDatabase('forex')) 79 | 80 | t.ok(await db.dropDatabase('forex')) 81 | t.notOk(await db.dropDatabase('forex')) 82 | 83 | await db.end() 84 | }) 85 | 86 | tape('createDatabase() without any previous database', async function (t) { 87 | const db = mysql(cfg[0], cfg[1], cfg[2], '') 88 | 89 | await db.ready() 90 | 91 | t.ok(await db.createDatabase('forex')) 92 | t.ok(await db.dropDatabase('forex')) 93 | 94 | await db.end() 95 | }) 96 | 97 | tape('createTable() and dropTable()', async function (t) { 98 | const db = mysql(...cfg) 99 | 100 | await db.ready() 101 | 102 | const columns = { 103 | username: { type: 'varchar', length: 16 } 104 | } 105 | 106 | t.ok(await db.createTable('users', columns)) 107 | t.notOk(await db.createTable('users', columns)) 108 | 109 | t.ok(await db.dropTable('users')) 110 | t.notOk(await db.dropTable('users')) 111 | 112 | await db.end() 113 | }) 114 | 115 | // + create complex tables 116 | 117 | tape('insert() without autoincrement id', async function (t) { 118 | const db = mysql(...cfg) 119 | 120 | await db.ready() 121 | 122 | await db.createTable('users', { 123 | id: { type: 'int', primary: true }, 124 | username: { type: 'varchar', length: 16 } 125 | }) 126 | 127 | t.ok(await db.insert('users', { id: 1, username: 'joe' }) === 0) 128 | t.ok(await db.insert('users', { id: 2, username: 'joe' }) === 0) 129 | 130 | await db.dropTable('users') 131 | 132 | await db.end() 133 | }) 134 | 135 | tape('insert() with autoincrement id', async function (t) { 136 | const db = mysql(...cfg) 137 | 138 | await db.ready() 139 | 140 | await db.createTable('users', { 141 | id: { type: 'int', increment: true, primary: true }, 142 | username: { type: 'varchar', length: 16 } 143 | }) 144 | 145 | t.ok(await db.insert('users', { username: 'joe' }) === 1) 146 | t.ok(await db.insert('users', { username: 'bob' }) === 2) 147 | 148 | await db.dropTable('users') 149 | 150 | await db.end() 151 | }) 152 | 153 | tape('insert()', async function (t) { 154 | const db = mysql(...cfg) 155 | 156 | await db.ready() 157 | 158 | await db.createTable('users', { 159 | username: { type: 'varchar', length: 16 } 160 | }, { 161 | unique: { 162 | user_username: ['username'] 163 | } 164 | }) 165 | 166 | await db.insert('users', { username: 'joe' }) 167 | 168 | try { 169 | await db.insert('users', { username: 'joe'.repeat(10) }) 170 | t.fail('inserted data that exceeds column length') 171 | } catch (error) { 172 | t.ok(error.code === 'ER_DATA_TOO_LONG') 173 | } 174 | 175 | try { 176 | await db.insert('users', { username: 'joe' }) 177 | t.fail('inserted a duplicated entry') 178 | } catch (error) { 179 | t.ok(error.code === 'ER_DUP_ENTRY') 180 | } 181 | 182 | await db.dropTable('users') 183 | 184 | await db.end() 185 | }) 186 | 187 | tape('select()', async function (t) { 188 | const db = mysql(...cfg) 189 | 190 | await db.ready() 191 | 192 | await db.createTable('users', { 193 | username: { type: 'varchar', length: 16 }, 194 | password: { type: 'varchar', length: 16 } 195 | }) 196 | await db.insert('users', { username: 'joe', password: '123' }) 197 | await db.insert('users', { username: 'bob', password: '456' }) 198 | 199 | const rows = await db.select('users') 200 | t.ok(rows.length === 2) 201 | t.deepEqual(rows[0], { username: 'joe', password: '123' }) 202 | t.deepEqual(rows[1], { username: 'bob', password: '456' }) 203 | 204 | const rows2 = await db.select('users', ['username']) 205 | t.ok(rows2.length === 2) 206 | t.deepEqual(rows2[0], { username: 'joe' }) 207 | t.deepEqual(rows2[1], { username: 'bob' }) 208 | 209 | const rows3 = await db.select('users', ['username'], 'LIMIT 1') 210 | t.ok(rows3.length === 1) 211 | t.deepEqual(rows3[0], { username: 'joe' }) 212 | 213 | const findUsername = 'joe' 214 | const rows4 = await db.select('users', ['password'], 'username = ?', findUsername) 215 | t.ok(rows4.length === 1) 216 | t.deepEqual(rows4[0], { password: '123' }) 217 | 218 | const rows5 = await db.select('users', ['*'], 'ORDER BY username ASC') 219 | t.ok(rows5.length === 2) 220 | t.ok(rows5[0].username === 'bob') 221 | t.ok(rows5[1].username === 'joe') 222 | 223 | const rows6 = await db.select('users', ['*'], 'ORDER BY username ASC LIMIT 1') 224 | t.ok(rows6.length === 1) 225 | t.ok(rows6[0].username === 'bob') 226 | 227 | const rows7 = await db.select('users', ['*'], 'username = ? ORDER BY username ASC LIMIT 2', 'joe') 228 | t.ok(rows7.length === 1) 229 | t.ok(rows7[0].username === 'joe') 230 | 231 | const rows8 = await db.select('users', ['*'], 'username = ?', 'random-username') 232 | t.ok(rows8.length === 0) 233 | 234 | const rows9 = await db.select('users', ['*'], 'username LIKE ?', 'b%') 235 | t.ok(rows9.length === 1) 236 | 237 | await db.dropTable('users') 238 | 239 | await db.end() 240 | }) 241 | 242 | tape('selectOne()', async function (t) { 243 | const db = mysql(...cfg) 244 | 245 | await db.ready() 246 | 247 | await db.createTable('users', { 248 | username: { type: 'varchar', length: 16 }, 249 | password: { type: 'varchar', length: 16 } 250 | }) 251 | await db.insert('users', { username: 'joe', password: '123' }) 252 | await db.insert('users', { username: 'bob', password: '456' }) 253 | 254 | const row = await db.selectOne('users', ['*'], 'username = ?', 'joe') 255 | t.deepEqual(row, { username: 'joe', password: '123' }) 256 | 257 | const row2 = await db.selectOne('users', ['*'], 'ORDER BY username ASC') 258 | t.deepEqual(row2, { username: 'bob', password: '456' }) 259 | 260 | const row3 = await db.selectOne('users', ['*'], 'username = ? ORDER BY username ASC', 'joe') 261 | t.deepEqual(row3, { username: 'joe', password: '123' }) 262 | 263 | const row4 = await db.selectOne('users', ['*'], 'username = ?', 'random-username') 264 | t.deepEqual(row4, undefined) 265 | 266 | await db.dropTable('users') 267 | 268 | await db.end() 269 | }) 270 | 271 | tape('exists()', async function (t) { 272 | const db = mysql(...cfg) 273 | 274 | await db.ready() 275 | 276 | await db.createTable('users', { 277 | username: { type: 'varchar', length: 16 }, 278 | password: { type: 'varchar', length: 16 } 279 | }) 280 | await db.insert('users', { username: 'joe', password: '123' }) 281 | await db.insert('users', { username: 'bob', password: '456' }) 282 | 283 | const exists = await db.exists('users', 'username = ?', 'joe') 284 | t.ok(exists) 285 | 286 | const exists2 = await db.exists('users', 'username = ?', 'random-username') 287 | t.notOk(exists2) 288 | 289 | await db.dropTable('users') 290 | 291 | await db.end() 292 | }) 293 | 294 | tape('count()', async function (t) { 295 | const db = mysql(...cfg) 296 | 297 | await db.ready() 298 | 299 | await db.createTable('users', { 300 | username: { type: 'varchar', length: 16 }, 301 | password: { type: 'varchar', length: 16 } 302 | }) 303 | await db.insert('users', { username: 'joe', password: '123' }) 304 | await db.insert('users', { username: 'bob', password: '456' }) 305 | 306 | const count = await db.count('users') 307 | t.ok(count === 2) 308 | 309 | const count2 = await db.count('users', 'username = ?', 'joe') 310 | t.ok(count2 === 1) 311 | 312 | const count3 = await db.count('users', 'username = ?', 'random-username') 313 | t.ok(count3 === 0) 314 | 315 | await db.dropTable('users') 316 | 317 | await db.end() 318 | }) 319 | 320 | tape('update()', async function (t) { 321 | const db = mysql(...cfg) 322 | 323 | await db.ready() 324 | 325 | await db.createTable('users', { 326 | username: { type: 'varchar', length: 16 }, 327 | password: { type: 'varchar', length: 16 } 328 | }) 329 | await db.insert('users', { username: 'joe', password: '123' }) 330 | await db.insert('users', { username: 'bob', password: '456' }) 331 | 332 | t.ok(await db.update('users', { username: 'alice' }, 'username = ?', 'bob') === 1) // 1 chg, 1 aff 333 | 334 | t.ok(await db.update('users', { username: 'alice' }) === 1) // 1 chg, 2 aff 335 | 336 | t.ok(await db.update('users', { username: 'alice' }, 'username = ?', 'random-username') === 0) // 0 chg, 0 aff 337 | 338 | t.ok(await db.update('users', { username: 'unique-username' }) === 2) 339 | t.ok(await db.update('users', { username: 'unique-username2' }, 'LIMIT 1') === 1) 340 | 341 | await db.dropTable('users') 342 | 343 | await db.end() 344 | }) 345 | 346 | tape('update() with arithmetic', async function (t) { 347 | const db = mysql(...cfg) 348 | 349 | await db.ready() 350 | 351 | await db.createTable('users', { 352 | username: { type: 'varchar', length: 16 }, 353 | count: { type: 'int' } 354 | }) 355 | await db.insert('users', { username: 'joe', count: 0 }) 356 | await db.insert('users', { username: 'bob', count: 0 }) 357 | 358 | t.deepEqual({ count: 0 }, await db.selectOne('users', ['count'], 'username = ?', 'bob')) 359 | t.ok(await db.update('users', [{ count: 'count + ?' }, 1], 'username = ?', 'bob') === 1) 360 | t.deepEqual({ count: 1 }, await db.selectOne('users', ['count'], 'username = ?', 'bob')) 361 | 362 | await db.dropTable('users') 363 | 364 | await db.end() 365 | }) 366 | 367 | tape('delete()', async function (t) { 368 | const db = mysql(...cfg) 369 | 370 | await db.ready() 371 | 372 | await db.createTable('users', { 373 | username: { type: 'varchar', length: 16 }, 374 | count: { type: 'int' } 375 | }) 376 | 377 | await db.insert('users', { username: 'joe', count: 0 }) 378 | await db.insert('users', { username: 'bob', count: 0 }) 379 | t.ok(await db.delete('users') === 2) 380 | t.ok(await db.delete('users') === 0) 381 | 382 | await db.insert('users', { username: 'joe', count: 0 }) 383 | await db.insert('users', { username: 'bob', count: 0 }) 384 | t.ok(await db.delete('users', 'LIMIT 1') === 1) 385 | t.ok(await db.delete('users') === 1) 386 | 387 | await db.insert('users', { username: 'joe', count: 0 }) 388 | await db.insert('users', { username: 'bob', count: 0 }) 389 | t.ok(await db.delete('users', 'username = ?', 'bob') === 1) 390 | t.ok(await db.delete('users', 'username = ?', 'bob') === 0) 391 | t.ok(await db.delete('users', 'username = ?', 'joe') === 1) 392 | 393 | await db.dropTable('users') 394 | 395 | await db.end() 396 | }) 397 | 398 | tape('transaction()', async function (t) { 399 | const db = mysql(...cfg) 400 | 401 | await db.ready() 402 | 403 | await db.createTable('users', { 404 | id: { type: 'int', increment: true, primary: true }, 405 | username: { type: 'varchar', length: 16 } 406 | }) 407 | await db.createTable('profiles', { 408 | owner: { type: 'int', primary: true }, 409 | name: { type: 'varchar', length: 16 } 410 | }) 411 | await db.insert('users', { username: 'joe' }) 412 | await db.insert('users', { username: 'bob' }) 413 | 414 | const resultId = await db.transaction(async function (conn) { 415 | const id = await conn.insert('users', { username: 'alice' }) 416 | await conn.insert('profiles', { owner: id, name: 'Alice' }) 417 | return id 418 | }) 419 | 420 | const user = await db.selectOne('users', ['*'], 'username = ?', 'alice') 421 | const profile = await db.selectOne('profiles', ['*'], 'owner = ?', user.id) 422 | t.ok(user.id === resultId) 423 | t.deepEqual(user, { id: 3, username: 'alice' }) 424 | t.deepEqual(profile, { owner: 3, name: 'Alice' }) 425 | 426 | await db.dropTable('users') 427 | await db.dropTable('profiles') 428 | 429 | await db.end() 430 | }) 431 | 432 | tape('transaction() with error', async function (t) { 433 | const db = mysql(...cfg) 434 | 435 | await db.ready() 436 | 437 | await db.createTable('users', { 438 | id: { type: 'int', increment: true, primary: true }, 439 | username: { type: 'varchar', length: 16 } 440 | }) 441 | await db.insert('users', { username: 'joe' }) 442 | await db.insert('users', { username: 'bob' }) 443 | 444 | try { 445 | await db.transaction(async function (conn) { 446 | await conn.delete('users', 'username = ?', 'joe') 447 | await conn.delete('users', 'username = ?', 'bob') 448 | throw new Error('test error') 449 | }) 450 | t.fail('transaction should have throw an error') 451 | } catch (error) { 452 | t.ok(error.message === 'test error') 453 | t.ok(await db.count('users') === 2) 454 | } 455 | 456 | await db.transaction(async function (conn) { 457 | await conn.delete('users', 'username = ?', 'joe') 458 | await conn.delete('users', 'username = ?', 'bob') 459 | }) 460 | t.ok(await db.count('users') === 0) 461 | 462 | await db.dropTable('users') 463 | 464 | await db.end() 465 | }) 466 | 467 | tape('execute()', async function (t) { 468 | const db = mysql(...cfg) 469 | 470 | await db.ready() 471 | 472 | await db.createTable('users', { 473 | id: { type: 'int', increment: true, primary: true }, 474 | username: { type: 'varchar', length: 16 } 475 | }) 476 | await db.insert('users', { username: 'joe' }) 477 | await db.insert('users', { username: 'bob' }) 478 | 479 | const [rows, fields] = await db.execute('SELECT * FROM `users` WHERE `username` = ?', ['joe']) 480 | t.deepEqual(rows, [{ id: 1, username: 'joe' }]) 481 | t.ok(fields[0].name === 'id') 482 | t.ok(fields[1].name === 'username') 483 | 484 | await db.dropTable('users') 485 | 486 | await db.end() 487 | }) 488 | 489 | tape('query()', async function (t) { 490 | const db = mysql(...cfg) 491 | 492 | await db.ready() 493 | 494 | await db.createTable('users', { 495 | id: { type: 'int', increment: true, primary: true }, 496 | username: { type: 'varchar', length: 16 } 497 | }) 498 | await db.insert('users', { username: 'joe' }) 499 | await db.insert('users', { username: 'bob' }) 500 | 501 | const [rows, fields] = await db.query('SELECT * FROM `users` WHERE `username` = "joe"') 502 | t.deepEqual(rows, [{ id: 1, username: 'joe' }]) 503 | t.ok(fields[0].name === 'id') 504 | t.ok(fields[1].name === 'username') 505 | 506 | await db.dropTable('users') 507 | 508 | await db.end() 509 | }) 510 | 511 | tape('without ready()', async function (t) { 512 | const db = mysql(...cfg) 513 | 514 | await db.createTable('users', { 515 | id: { type: 'int', increment: true, primary: true }, 516 | username: { type: 'varchar', length: 16 } 517 | }) 518 | await db.insert('users', { username: 'joe' }) 519 | await db.insert('users', { username: 'bob' }) 520 | 521 | const [rows, fields] = await db.execute('SELECT * FROM `users` WHERE `username` = ?', ['joe']) 522 | t.deepEqual(rows, [{ id: 1, username: 'joe' }]) 523 | t.ok(fields[0].name === 'id') 524 | t.ok(fields[1].name === 'username') 525 | 526 | await db.dropTable('users') 527 | 528 | await db.end() 529 | }) 530 | 531 | /* 532 | readme: 533 | 534 | // CREATE DATABASE IF NOT EXISTS `myapp` ... 535 | await db.createDatabase('myapp') 536 | 537 | // CREATE TABLE IF NOT EXISTS `myapp`.`ips` (...) 538 | await db.createTable('ips', { 539 | id: { type: 'int', unsigned: true, increment: true, primary: true }, 540 | addr: { type: 'varchar', length: 16, required: true, index: true }, 541 | hits: { type: 'int', unsigned: true, default: 0 } 542 | }) 543 | 544 | // DROP TABLE IF EXISTS `myapp`.`ips` 545 | await db.dropTable('ips') 546 | 547 | // DROP DATABASE IF EXISTS `myapp` 548 | await db.dropDatabase('myapp') 549 | 550 | test: 551 | 552 | await db.createTable('items', { 553 | id: { type: 'int', unsigned: true, required: true, increment: true, primary: true }, 554 | fullname: { type: 'varchar', length: 64, collate: 'utf8mb4_unicode_ci', required: true }, 555 | description: { type: 'varchar', length: 256, required: true }, 556 | price: { type: 'decimal', length: [11, 2], required: true }, 557 | dni: { type: 'int', unsigned: true, required: true }, 558 | birthdate: { type: 'date', default: null }, 559 | liq: { type: 'decimal(11,2)', required: true }, 560 | cuit: { type: 'bigint', unsigned: true, required: true }, 561 | posted: { type: 'tinyint', unsigned: true, required: true } 562 | }, { 563 | unique: { 564 | person_dni: ['dni', 'fullname'] 565 | }, 566 | index: { 567 | person2: ['fullname ASC', 'dni ASC'] 568 | }, 569 | engine: 'InnoDB', 570 | increment: 95406, 571 | charset: 'utf8mb4', 572 | collate: 'utf8mb4_unicode_ci' 573 | }) 574 | */ 575 | --------------------------------------------------------------------------------