sharp-db
Version:
Classes for running SQL and building select queries for MySQL in Node
1,432 lines (1,430 loc) • 44.4 kB
JavaScript
jest.mock('mysql2');
const mysqlMock = require('mysql2');
jest.mock('ssh2');
const ssh2Mock = require('ssh2');
const Db = require('./Db.js');
const Ssh = require('../Ssh/Ssh.js');
describe('Db', () => {
let db;
beforeEach(() => {
Db.instances.length = 0;
db = Db.factory();
mysqlMock.reset();
});
describe('class', () => {
it('should be instantiable', () => {
expect(db).toBeInstanceOf(Db);
});
it('should allow factory()', () => {
const db = Db.factory();
expect(db).toBeInstanceOf(Db);
});
it('should work as singleton', () => {
const db1 = Db.factory();
const db2 = Db.factory();
expect(db1).toBe(db2);
});
it('should work with "new"', () => {
const db = new Db();
expect(db).toBeInstanceOf(Db);
});
it('should connect via SSH', () => {
const db = new Db(
{
password: '',
},
{
user: 'ubuntu',
password: 'moo',
}
);
expect(db.ssh).toBeInstanceOf(Ssh);
});
});
describe('binding', () => {
it('should bind strings', () => {
const bound = db.bindArgs('WHERE a = ?', ['foo']);
expect(bound.sql).toBe("WHERE a = 'foo'");
});
it('should bind String object', () => {
const bound = db.bindArgs('WHERE a = ?', [new String('foo')]);
expect(bound.sql).toBe("WHERE a = 'foo'");
});
it('should bind numbers', () => {
const bound = db.bindArgs('WHERE a = ? AND b = ?', [1, 2]);
expect(bound.sql).toBe('WHERE a = 1 AND b = 2');
});
it('should bind Number object', () => {
const bound = db.bindArgs('WHERE a = ? AND b = ?', [
new Number(1),
new Number(2),
]);
expect(bound.sql).toBe('WHERE a = 1 AND b = 2');
});
it('should bind true', () => {
const bound = db.bindArgs('WHERE is_success = :isSuccess', [
{
isSuccess: true,
},
]);
expect(bound.sql).toBe('WHERE is_success = true');
});
it('should bind false', () => {
const bound = db.bindArgs('WHERE is_active = ?', [false]);
expect(bound.sql).toBe('WHERE is_active = false');
});
it('should bind Boolean true Object', () => {
const bound = db.bindArgs('WHERE is_active = ?', [new Boolean(true)]);
expect(bound.sql).toBe('WHERE is_active = true');
});
it('should bind Boolean false Object', () => {
const bound = db.bindArgs('WHERE is_active = ?', [new Boolean(false)]);
expect(bound.sql).toBe('WHERE is_active = false');
});
it('should bind arrays', () => {
const bound = db.bindArgs('WHERE id IN(?)', [[1, 2, 3]]);
expect(bound.sql).toBe('WHERE id IN(1, 2, 3)');
});
it('should bind nulls', () => {
const bound = db.bindArgs('SET a = ?', [null]);
expect(bound.sql).toBe('SET a = NULL');
});
it('should bind Date Objects', () => {
const now = '2021-01-25 10:34:36.472';
const date = new Date('2021-01-25 10:34:36.472');
const bound = db.bindArgs('SET at = ?', [date]);
expect(bound.sql).toBe(`SET at = '${now}'`);
});
it('should do nothing on empty arrays', () => {
const bound = db.bindArgs('SET a = 1', []);
expect(bound.sql).toBe('SET a = 1');
});
it('should do nothing on undefined', () => {
const bound = db.bindArgs('SET a = ?', undefined);
expect(bound.sql).toBe('SET a = ?');
});
it('should throw errors if SQL is empty string', () => {
const bindEmpty = () => {
db.bindArgs('', []);
};
expect(bindEmpty).toThrow();
});
it('should throw errors if SQL is empty object', () => {
const bindEmpty = () => {
db.bindArgs({}, []);
};
expect(bindEmpty).toThrow();
});
});
describe('connect()', () => {
it('should handle errors', async () => {
const error = new Error('foo');
error.fatal = true;
mysqlMock.pushConnect(error);
try {
await db.connect();
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e.fatal).toBe(true);
}
});
});
describe('select()', () => {
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.select('SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
it('should return result arrays', async () => {
const sql = 'SELECT * FROM users';
const mockResults = [
{ name: 'John Doe', email: 'john@example.com' },
{ name: 'Jane Doe', email: 'jane@example.com' },
];
const mockFields = [{ name: 'name' }, { name: 'email' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { query, results, fields } = await db.select(sql);
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
expect(fields).toEqual(mockFields);
});
it('should bind on question marks', async () => {
const sql = 'SELECT * FROM users WHERE id BETWEEN ? AND ?';
const { query } = await db.select(sql, 101, 199);
expect(query).toBe('SELECT * FROM users WHERE id BETWEEN 101 AND 199');
});
it('should bind on colons', async () => {
const sql = 'SELECT * FROM users WHERE id BETWEEN :min AND :max';
const { query } = await db.select(sql, { min: 101, max: 199 });
expect(query).toBe('SELECT * FROM users WHERE id BETWEEN 101 AND 199');
});
it('should handle options', async () => {
const sql = 'SELECT * FROM users';
const { query } = await db.select({ sql, timeout: 30000 });
expect(query).toBe('SELECT * FROM users');
});
it('should handle array values bound in options', async () => {
const sql = 'SELECT * FROM users WHERE id BETWEEN ? AND ?';
const { query } = await db.select({ sql, values: [101, 199] });
expect(query).toBe('SELECT * FROM users WHERE id BETWEEN 101 AND 199');
});
it('should handle array options binding and param binding', async () => {
const sql =
'SELECT * FROM users WHERE id BETWEEN ? AND ? AND department_id = ?';
const { query } = await db.select({ sql, values: [101, 199] }, 1);
expect(query).toBe(
'SELECT * FROM users WHERE id BETWEEN 101 AND 199 AND department_id = 1'
);
});
it('should handle objects bound in options', async () => {
const sql = 'SELECT * FROM users WHERE id BETWEEN :min AND :max';
const { query } = await db.select({
sql,
values: { min: 101, max: 199 },
});
expect(query).toBe('SELECT * FROM users WHERE id BETWEEN 101 AND 199');
});
it('should handle object options binding and param binding', async () => {
const sql =
'SELECT * FROM users WHERE id BETWEEN :min AND :max AND department = ?';
const { query } = await db.select(
{ sql, values: { min: 101, max: 199 } },
1
);
expect(query).toBe(
'SELECT * FROM users WHERE id BETWEEN 101 AND 199 AND department = 1'
);
});
it('should tunnel via SSH', async () => {
const stream = {};
ssh2Mock.pushResponse({
err: null,
stream,
});
mysqlMock.pushResponse({ results: [] });
const db = new Db(
{
password: '',
},
{
user: 'ubuntu',
password: 'moo',
}
);
const { results } = await db.select('SELECT * FROM foo');
expect(results).toEqual([]);
});
});
describe('selectHash()', () => {
it('should return result object', async () => {
const sql = 'SELECT email, name FROM users';
const mockResults = [
{ name: 'John Doe', email: 'john@example.com' },
{ name: 'Jane Doe', email: 'jane@example.com' },
];
const mockFields = [{ name: 'email' }, { name: 'name' }];
const hash = {
'john@example.com': 'John Doe',
'jane@example.com': 'Jane Doe',
};
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectHash(sql);
expect(results).toEqual(hash);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectHash('SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectList()', () => {
it('should return item list', async () => {
const sql = 'SELECT name FROM users';
const mockResults = [{ name: 'John Doe' }, { name: 'Jane Doe' }];
const mockFields = [{ name: 'name' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectList(sql);
expect(results).toEqual([mockResults[0].name, mockResults[1].name]);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectList('SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectGrouped()', () => {
it('should return result object with lists', async () => {
const sql = 'SELECT email, name FROM users';
const mockResults = [
{ name: 'John Doe', department_id: 1 },
{ name: 'Jane Doe', department_id: 2 },
{ name: 'Josh Doe', department_id: 1 },
];
const hash = {
1: [mockResults[0], mockResults[2]],
2: [mockResults[1]],
};
mysqlMock.pushResponse({ results: mockResults });
const { results } = await db.selectGrouped('department_id', sql);
expect(results).toEqual(hash);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectGrouped('department_id', 'SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectIndexed()', () => {
it('should return result object', async () => {
const sql = 'SELECT email, name FROM users';
const mockResults = [
{ name: 'John Doe', email: 'john@example.com' },
{ name: 'Jane Doe', email: 'jane@example.com' },
];
const hash = {
'john@example.com': mockResults[0],
'jane@example.com': mockResults[1],
};
mysqlMock.pushResponse({ results: mockResults });
const { results } = await db.selectIndexed('email', sql);
expect(results).toEqual(hash);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectIndexed('id', 'SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectFirst()', () => {
it('should return first result object', async () => {
const sql = 'SELECT email, name FROM users';
const mockResults = [
{ name: 'John Doe', email: 'john@example.com' },
{ name: 'Jane Doe', email: 'jane@example.com' },
];
mysqlMock.pushResponse({ results: mockResults });
const { results } = await db.selectFirst(sql);
expect(results).toEqual(mockResults[0]);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectFirst('SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectValue()', () => {
it('should return the first field from the first row', async () => {
const sql = 'SELECT name FROM users';
const mockResults = [{ name: 'John Doe' }, { name: 'Jane Doe' }];
const mockFields = [{ name: 'name' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectValue(sql);
expect(results).toEqual(mockResults[0].name);
});
it('should return undefined on empty results', async () => {
const sql = 'SELECT name FROM users';
const mockResults = [];
const mockFields = [{ name: 'name' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectValue(sql);
expect(results).toBe(undefined);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectValue('SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectExists()', () => {
it('should construct a SELECT EXISTS query', async () => {
const sql = 'SELECT name FROM users';
const mockResults = [{ does_it_exist: true }];
const mockFields = [];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectExists(sql);
expect(results).toBe(true);
});
it('should return false if there are no results', async () => {
const sql = 'SELECT name FROM users';
const mockResults = [];
const mockFields = [];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectExists(sql);
expect(results).toBe(false);
});
it('should construct a SELECT EXISTS query from object', async () => {
const sql = 'SELECT name FROM users';
const mockResults = [{ does_it_exist: true }];
const mockFields = [];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.selectExists({ sql });
expect(results).toBe(true);
});
it('should handle errors', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.selectExists('SELECT * FROM users');
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('insert()', () => {
it('should return the id of the inserted record', async () => {
const sql = 'INSERT INTO users VALUES(?)';
const mockResults = { insertId: 1 };
mysqlMock.pushResponse({ results: mockResults });
const { insertId } = await db.insert(sql, 'John Doe');
expect(insertId).toBe(1);
});
it('should reject on error', async () => {
const sql = 'INSERT INTO foo VALUES (1);';
mysqlMock.pushResponse({ error: new Error('bar') });
try {
await db.insert(sql);
} catch (e) {
expect(e.message).toContain('bar');
}
});
});
describe('update()', () => {
it('should return the number of changed rows', async () => {
const sql = 'UPDATE users SET department_id = 2 WHERE department_id = 1';
const mockResults = { changedRows: 3 };
mysqlMock.pushResponse({ results: mockResults });
const { changedRows } = await db.update(sql);
expect(changedRows).toBe(3);
});
it('should reject on error', async () => {
const sql = 'UPDATE users SET is_active = true';
mysqlMock.pushResponse({ error: new Error('foo') });
try {
await db.update(sql);
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('delete()', () => {
it('should return the number of deleted rows', async () => {
const sql = 'DELETE FROM users WHERE department_id = 1';
const mockResults = { changedRows: 3 };
mysqlMock.pushResponse({ results: mockResults });
const { changedRows } = await db.delete(sql);
expect(changedRows).toBe(3);
});
it('should reject on error', async () => {
const sql = 'DELETE FROM users WHERE is_active = false';
mysqlMock.pushResponse({ error: new Error('foo') });
try {
await db.delete(sql);
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('selectFrom()', () => {
it('should handle empty fields and criteria', async () => {
const { query } = await db.selectFrom('users');
expect(query).toBe('SELECT * FROM `users` WHERE 1');
});
it('should handle named fields', async () => {
const { query } = await db.selectFrom('users', ['id', 'name']);
expect(query).toBe('SELECT `id`, `name` FROM `users` WHERE 1');
});
it('should handle expression fields', async () => {
const { query } = await db.selectFrom('users', [
'id',
"CONCAT(fname, ' ', lname)",
]);
expect(query).toBe(
"SELECT `id`, CONCAT(fname, ' ', lname) FROM `users` WHERE 1"
);
});
it('should handle numeric criteria', async () => {
const { query } = await db.selectFrom('users', [], { id: 1 });
expect(query).toBe('SELECT * FROM `users` WHERE `id` = 1');
});
it('should handle boolean criteria', async () => {
const { query } = await db.selectFrom('users', [], { is_active: false });
expect(query).toBe('SELECT * FROM `users` WHERE `is_active` = false');
});
it('should handle null criteria', async () => {
const { query } = await db.selectFrom('users', [], { deleted_at: null });
expect(query).toBe('SELECT * FROM `users` WHERE `deleted_at` IS NULL');
});
it('should handle string criteria', async () => {
const { query } = await db.selectFrom('users', [], {
email: 'john@example.com',
});
expect(query).toBe(
"SELECT * FROM `users` WHERE `email` = 'john@example.com'"
);
});
it('should handle array criteria', async () => {
const { query } = await db.selectFrom('users', [], { id: [1, 2] });
expect(query).toBe('SELECT * FROM `users` WHERE `id` IN(1,2)');
});
it('should handle "extra"', async () => {
const { query } = await db.selectFrom('users', [], {}, 'LIMIT 5');
expect(query).toBe('SELECT * FROM `users` WHERE 1 LIMIT 5');
});
it('should error when fields are not an array', () => {
const tryNumber = () => {
db.selectFrom('users', 2, {});
};
expect(tryNumber).toThrow();
});
it('should error when criteria is not an object', () => {
const tryNumber = () => {
db.selectFrom('users', [], 7);
};
expect(tryNumber).toThrow();
});
});
describe('selectId()', () => {
it('should return the correct result object', async () => {
const mockResults = [{ id: 2, name: 'Jane Doe' }];
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.selectId('users', mockResults[0].id);
expect(results).toEqual(mockResults[0]);
expect(query).toBe('SELECT * FROM `users` WHERE `id` = 2');
});
});
describe('selectUuid()', () => {
it('should return the correct result object', async () => {
const mockResults = [
{ uuid: '4baf1860-cf2b-4037-8ad3-6e043cc144d9', name: 'Jane Doe' },
];
mysqlMock.pushResponse({ results: mockResults });
const { results, query } = await db.selectUuid(
'users',
mockResults[0].uuid
);
expect(results).toBe(mockResults[0]);
expect(query).toBe(
"SELECT * FROM `users` WHERE `uuid` = '4baf1860-cf2b-4037-8ad3-6e043cc144d9'"
);
});
});
describe('selectByKey()', () => {
it('should return the correct result object', async () => {
const mockResults = [{ sso_ref: 'A123456', name: 'Jane Doe' }];
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.selectByKey(
'users',
'sso_ref',
mockResults[0].sso_ref
);
expect(results).toEqual(mockResults[0]);
expect(query).toBe("SELECT * FROM `users` WHERE `sso_ref` = 'A123456'");
});
});
describe('selectOrCreate()', () => {
it('should return the correct result object', async () => {
const mockResults = [{ sso_ref: 'A123456', name: 'Jane Doe' }];
mysqlMock.pushResponse({ results: mockResults });
const { results } = await db.selectOrCreate(
'users',
{ sso_ref: mockResults[0].sso_ref },
mockResults[0]
);
expect(results).toEqual(mockResults[0]);
});
it('should insert a new record', async () => {
const newRow = {
sso_ref: 'A123456',
name: 'Jane Doe',
id: 5,
};
mysqlMock.pushResponse({ results: [] });
mysqlMock.pushResponse({ results: { insertId: 5 } });
mysqlMock.pushResponse({ results: [newRow] });
const { insertId } = await db.selectOrCreate(
'users',
{ sso_ref: 'A123456' },
{ sso_ref: 'A123456', name: 'Jane Doe' }
);
expect(insertId).toBe(5);
});
it('should throw error if insertId is falsy', async () => {
let error;
try {
mysqlMock.pushResponse({ results: [] });
mysqlMock.pushResponse({ results: {} });
await db.selectOrCreate(
'users',
{ sso_ref: 'A123456' },
{ sso_ref: 'A123456', name: 'Jane Doe' }
);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(Error);
});
it('should return new row', async () => {
const newRow = {
sso_ref: 'A123456',
name: 'Jane Doe',
id: 5,
};
mysqlMock.pushResponse({ results: [] });
mysqlMock.pushResponse({ results: { insertId: 5 } });
mysqlMock.pushResponse({ results: [newRow] });
const { results, insertId } = await db.selectOrCreate(
'users',
{ sso_ref: 'A123456' },
{ sso_ref: 'A123456', name: 'Jane Doe' }
);
expect(insertId).toBe(5);
expect(results).toEqual(newRow);
});
it('should throw error if new row is falsy', async () => {
let error;
try {
mysqlMock.pushResponse({ results: [] });
mysqlMock.pushResponse({ results: { insertId: 5 } });
mysqlMock.pushResponse({
results: false,
});
await db.selectOrCreate(
'users',
{ sso_ref: 'A123456' },
{ sso_ref: 'A123456', name: 'Jane Doe' }
);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(Error);
});
it('should throw error if initial select fails', async () => {
let error;
try {
await db.selectOrCreate('users', 'invalid', {
sso_ref: 'A123456',
name: 'Jane Doe',
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(Error);
});
});
describe('selectOrCreateId()', () => {
it('should return the id of the new record', async () => {
mysqlMock.pushResponse({ results: [] });
mysqlMock.pushResponse({ results: { insertId: 5 } });
const { results } = await db.selectOrCreateId(
'users',
{ sso_ref: 'A123456' },
{ sso_ref: 'A123456', name: 'Jane Doe' }
);
expect(results).toBe(5);
});
it('should return the record if existing', async () => {
mysqlMock.pushResponse({ results: [{ a: 1, b: 2, id: 5 }] });
const { results } = await db.selectOrCreateId(
'users',
{ a: 1 },
{ a: 1, b: 2 }
);
expect(results).toBe(5);
});
it('should throw error if initial select fails', async () => {
let error;
try {
mysqlMock.pushResponse({ results: null });
await db.selectOrCreateId(
'users',
{ sso_ref: 'A123456' },
{ sso_ref: 'A123456', name: 'Jane Doe' }
);
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(Error);
});
});
describe('insertInto()', () => {
it('should return the id of the inserted record', async () => {
const mockResults = { insertId: 5 };
mysqlMock.pushResponse({ results: mockResults });
const { query, insertId } = await db.insertInto('users', {
sso_ref: 'A123456',
name: 'Jane Doe',
});
expect(insertId).toBe(5);
expect(query).toBe(
"INSERT INTO `users` SET `sso_ref`='A123456', `name`='Jane Doe'"
);
});
it('should error if data is empty', () => {
const doInsert = () => {
db.insertInto('users', {});
};
expect(doInsert).toThrow();
});
});
describe('insertIntoOnDuplicateKeyUpdate()', () => {
it('should return last insert and affected', async () => {
const mockResults = { insertId: 5, affectedRows: 1 };
mysqlMock.pushResponse({ results: mockResults });
const { query, insertId, affectedRows } =
await db.insertIntoOnDuplicateKeyUpdate(
'users',
{
sso_ref: 'A123456',
name: 'Jane Doe',
created_at: '2020-02-02',
},
{
name: 'Jane Doe',
modified_at: '2020-02-02',
}
);
expect(insertId).toBe(5);
expect(affectedRows).toBe(1);
expect(query).toBe(
"INSERT INTO `users` SET `sso_ref`='A123456', `name`='Jane Doe', `created_at`='2020-02-02' ON DUPLICATE KEY UPDATE `name`='Jane Doe', `modified_at`='2020-02-02'"
);
});
it('should error if inserts are empty', async () => {
try {
await db.insertIntoOnDuplicateKeyUpdate('test', {}, { a: 1 });
} catch (e) {
expect(e).toBeInstanceOf(Error);
}
});
it('should error if inserts are undefined', async () => {
try {
await db.insertIntoOnDuplicateKeyUpdate('test', undefined, { a: 1 });
} catch (e) {
expect(e).toBeInstanceOf(Error);
}
});
it('should error if sets are empty', async () => {
try {
await db.insertIntoOnDuplicateKeyUpdate('test', { a: 1 }, {});
} catch (e) {
expect(e).toBeInstanceOf(Error);
}
});
it('should error if sets are undefined', async () => {
try {
await db.insertIntoOnDuplicateKeyUpdate('test', { a: 1 });
} catch (e) {
expect(e).toBeInstanceOf(Error);
}
});
it('should error if sets are undefined', async () => {
mysqlMock.pushResponse({
error: new Error('foo'),
});
try {
await db.insertIntoOnDuplicateKeyUpdate('test', { a: 1 }, { b: 2 });
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('insertExtended()', () => {
it('should return last insert and affected', async () => {
const mockResults = { insertId: 5, affectedRows: 2 };
mysqlMock.pushResponse({ results: mockResults });
const { query, insertId, affectedRows } = await db.insertExtended(
'users',
[
{ name: 'John Doe', email: 'john@example.com' },
{ name: 'Jane Doe', email: 'jane@example.com' },
]
);
expect(insertId).toBe(5);
expect(affectedRows).toBe(2);
expect(query).toBe(
"INSERT INTO `users` (`name`, `email`) VALUES ('John Doe', 'john@example.com'), ('Jane Doe', 'jane@example.com')"
);
});
it('should error if inserts are empty', async () => {
const useEmpty = () => {
db.insertExtended('users', []);
};
expect(useEmpty).toThrow();
});
it('should error if inserts are undefined', async () => {
const useUndefined = () => {
db.insertExtended('users', undefined);
};
expect(useUndefined).toThrow();
});
});
describe('updateTable()', () => {
it('should return affected', async () => {
const mockResults = { affectedRows: 1 };
mysqlMock.pushResponse({ results: mockResults });
const { query, affectedRows } = await db.updateTable(
'users',
{ foo: undefined, email: 'john@example.com' },
{ id: 5 }
);
expect(affectedRows).toBe(1);
expect(query).toBe(
"UPDATE `users` SET `email`='john@example.com' WHERE `id` = 5"
);
});
it('should work without criteria', async () => {
const mockResults = { affectedRows: 100 };
mysqlMock.pushResponse({ results: mockResults });
const { query, affectedRows } = await db.updateTable('users', {
is_active: true,
});
expect(affectedRows).toBe(100);
expect(query).toBe('UPDATE `users` SET `is_active`=true WHERE 1');
});
it('should error on empty object', async () => {
const tryUpdate = () => {
db.updateTable('users', {});
};
expect(tryUpdate).toThrow();
});
it('should error on undefined', async () => {
const tryUpdate = () => {
db.updateTable('users');
};
expect(tryUpdate).toThrow();
});
});
describe('deleteFrom()', () => {
it('should return affected', async () => {
const mockResults = { affectedRows: 1 };
mysqlMock.pushResponse({ results: mockResults });
const { query, affectedRows } = await db.deleteFrom('users', {
email: 'john@example.com',
});
expect(affectedRows).toBe(1);
expect(query).toBe(
"DELETE FROM `users` WHERE `email` = 'john@example.com'"
);
});
it('should return affected (with limit)', async () => {
const mockResults = { affectedRows: 1 };
mysqlMock.pushResponse({ results: mockResults });
const { query, affectedRows } = await db.deleteFrom(
'users',
{ email: 'john@example.com' },
1
);
expect(affectedRows).toBe(1);
expect(query).toBe(
"DELETE FROM `users` WHERE `email` = 'john@example.com' LIMIT 1"
);
});
});
describe('tpl() escaping', () => {
it('should template Numbers', async () => {
const { select } = db.tpl();
const id = 4;
const { query } = await select`SELECT * FROM users WHERE id = ${id}`;
expect(query).toBe('SELECT * FROM users WHERE id = 4');
});
it('should template Strings', async () => {
const { select } = db.tpl();
const email = 'john@example.com';
const { query } =
await select`SELECT * FROM users WHERE email = ${email}`;
expect(query).toBe(
"SELECT * FROM users WHERE email = 'john@example.com'"
);
});
it('should template Booleans', async () => {
const { select } = db.tpl();
const isActive = true;
const { query } =
await select`SELECT * FROM users WHERE is_active = ${isActive}`;
expect(query).toBe('SELECT * FROM users WHERE is_active = true');
});
it('should template Arrays', async () => {
const { select } = db.tpl();
const ids = [1, 3];
const { query } = await select`SELECT * FROM users WHERE id IN(${ids})`;
expect(query).toBe('SELECT * FROM users WHERE id IN(1, 3)');
});
it('should cache templating', async () => {
const { select: select1 } = db.tpl();
const { select: select2 } = db.tpl();
expect(select1).toBe(select2);
});
});
describe('tpl() functions', () => {
it('should allow selectFirst', async () => {
const { selectFirst } = db.tpl();
const id = 4;
const { query } = await selectFirst`SELECT * FROM users WHERE id = ${id}`;
expect(query).toBe('SELECT * FROM users WHERE id = 4');
});
it('should allow selectList', async () => {
const mockResults = [
{ email: 'john@example.com' },
{ email: 'jane@example.com' },
];
const mockFields = [{ name: 'email' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { selectList } = db.tpl();
const id = 4;
const { query } =
await selectList`SELECT email FROM users WHERE id > ${id}`;
expect(query).toBe('SELECT email FROM users WHERE id > 4');
});
it('should allow selectHash', async () => {
const mockResults = [
{ id: 4, name: 'John Doe' },
{ id: 5, name: 'Jane Doe' },
];
const mockFields = [{ name: 'id' }, { name: 'name' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { selectHash } = db.tpl();
const id = 4;
const { query } =
await selectHash`SELECT id, name FROM users WHERE id > ${id}`;
expect(query).toBe('SELECT id, name FROM users WHERE id > 4');
});
it('should allow selectValue', async () => {
const mockResults = [{ email: 'jane@example.com' }];
const mockFields = [{ name: 'email' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { selectValue } = db.tpl();
const id = 4;
const { query } =
await selectValue`SELECT email FROM users WHERE id = ${id}`;
expect(query).toBe('SELECT email FROM users WHERE id = 4');
});
it('should allow insert', async () => {
const { insert } = db.tpl();
const name = 'Jane Doe';
const email = 'jane@example.com';
const { query } =
await insert`INSERT INTO users VALUES (${name}, ${email})`;
expect(query).toBe(
"INSERT INTO users VALUES ('Jane Doe', 'jane@example.com')"
);
});
it('should allow update', async () => {
const { update } = db.tpl();
const name = 'Jane Doe';
const email = 'jane@example.com';
const { query } =
await update`UPDATE users SET name = ${name} WHERE email = ${email}`;
expect(query).toBe(
"UPDATE users SET name = 'Jane Doe' WHERE email = 'jane@example.com'"
);
});
it('should allow delete', async () => {
const { delete: del } = db.tpl();
const id = 4;
const { query } = await del`DELETE FROM users WHERE id = ${id}`;
expect(query).toBe('DELETE FROM users WHERE id = 4');
});
});
describe('query()', () => {
it('should return results', async () => {
const sql = 'UPDATE users SET is_active = true';
const mockResults = { affectedRows: 1 };
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.query(sql);
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
});
it('should return fields', async () => {
const sql = 'UPDATE users SET is_active = true';
const mockResults = [{ id: 1 }, { id: 2 }];
const mockFields = [{ name: 'id' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results, fields } = await db.query(sql);
expect(results).toEqual(mockResults);
expect(fields).toEqual(mockFields);
});
it('should reject on error', async () => {
const sql = 'UPDATE users SET is_active = true';
mysqlMock.pushResponse({ error: new Error('foo') });
try {
await db.query(sql);
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('multiQuery()', () => {
it('should return multiple results', async () => {
const sql = 'UPDATE users SET is_active = true; SELECT * FROM users';
const mockResults = [{ affectedRows: 1 }, { id: 1, name: 'John' }];
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.multiQuery(sql);
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
});
it('should reject on error', async () => {
const sql = 'UPDATE users SET is_active = true; SELECT * FROM posts';
mysqlMock.pushResponse({ error: new Error('foo') });
try {
await db.query(sql);
} catch (e) {
expect(e.message).toContain('foo');
}
});
});
describe('exportAsSql()', () => {
it('should build export string', async () => {
const mockResults = [
{ id: 1, fname: 'John' },
{ id: 2, fname: 'Jane' },
];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results, fields, query, affectedRows, chunks } =
await db.exportAsSql('users');
expect(results).toBe(
`
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(1,'John'),
(2,'Jane');
`.trim()
);
expect(fields).toEqual(mockFields);
expect(query).toBe('SELECT * FROM `users` WHERE 1');
expect(affectedRows).toBe(2);
expect(chunks).toBe(1);
});
it('should export 0 records', async () => {
const mockResults = [];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results, fields, query, affectedRows, chunks } =
await db.exportAsSql('users');
expect(results).toBe('');
expect(fields).toEqual(mockFields);
expect(query).toBe('SELECT * FROM `users` WHERE 1');
expect(affectedRows).toBe(0);
expect(chunks).toBe(0);
});
it('should discard ids', async () => {
const mockResults = [
{ id: 1, fname: 'John' },
{ id: 2, fname: 'Jane' },
];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.exportAsSql(
'users',
{},
{ discardIds: true, limit: 5 }
);
expect(results).toBe(
`
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(NULL,'John'),
(NULL,'Jane');
`.trim()
);
});
it('should add truncate statement', async () => {
const mockResults = [
{ id: 1, fname: 'John' },
{ id: 2, fname: 'Jane' },
];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.exportAsSql(
'users',
{},
{ truncateTable: true }
);
expect(results).toBe(
`
TRUNCATE TABLE \`users\`;
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(1,'John'),
(2,'Jane');
`.trim()
);
});
it('should add lock tables statement', async () => {
const mockResults = [
{ id: 1, fname: 'John' },
{ id: 2, fname: 'Jane' },
];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.exportAsSql(
'users',
{},
{ lockTables: true }
);
expect(results).toBe(
`
LOCK TABLES \`users\` WRITE;
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(1,'John'),
(2,'Jane');
UNLOCK TABLES;
`.trim()
);
});
it('should disable and re-enable foreign key checks', async () => {
const mockResults = [
{ id: 1, fname: 'John' },
{ id: 2, fname: 'Jane' },
];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.exportAsSql(
'users',
{},
{ disableForeignKeyChecks: true }
);
expect(results).toBe(
`
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(1,'John'),
(2,'Jane');
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
`.trim()
);
});
it('should split inserts into chunks', async () => {
const mockResults = [
{ id: 1, fname: 'John' },
{ id: 2, fname: 'Jane' },
];
const mockFields = [{ name: 'id' }, { name: 'fname' }];
mysqlMock.pushResponse({ results: mockResults, fields: mockFields });
const { results } = await db.exportAsSql('users', {}, { chunkSize: 1 });
expect(results).toBe(
`
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(1,'John');
INSERT INTO \`users\` (\`id\`,\`fname\`) VALUES
(2,'Jane');
`.trim()
);
});
});
describe('transactions', () => {
it('should start transaction', async () => {
mysqlMock.pushResponse({});
const { query } = await db.startTransaction();
await db.end();
expect(query).toBe('START TRANSACTION');
});
it('should begin transaction', async () => {
mysqlMock.pushResponse({});
const { query } = await db.beginTransaction();
await db.end();
expect(query).toBe('START TRANSACTION');
});
it('should commit', async () => {
mysqlMock.pushResponse({});
const { query } = await db.commit();
await db.end();
expect(query).toBe('COMMIT');
});
it('should rollback', async () => {
mysqlMock.pushResponse({});
const { query } = await db.rollback();
await db.end();
expect(query).toBe('ROLLBACK');
});
});
describe('end()', () => {
it('should not error on query', async () => {
const sql = 'UPDATE posts SET is_active = true';
const mockResults = { affectedRows: 2 };
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.query(sql);
await db.end();
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
});
it('should allow endAll', async () => {
const sql = 'UPDATE users SET is_active = true';
const mockResults = { affectedRows: 3 };
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.query(sql);
await Db.endAll();
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
});
it('should not error even if not connected', async () => {
let err = undefined;
try {
await db.end();
} catch (e) {
err = e;
}
expect(err).toBe(undefined);
});
it('should not error if Db.instances is already empty', async () => {
let err = undefined;
try {
await db.connect();
Db.instances.length = 0;
await db.end();
} catch (e) {
err = e;
}
expect(err).toBe(undefined);
});
it('should end when db.ssh is defined', async () => {
const spy = jest.fn();
ssh2Mock.onNextEnd(spy);
ssh2Mock.pushResponse({
err: null,
stream: {},
});
const db = new Db(
{
password: '',
},
{
user: 'ubuntu',
password: 'moo',
}
);
await db.connect();
await db.end();
expect(spy).toHaveBeenCalled();
});
it('should end when db.ssh is defined', async () => {
mysqlMock.pushEnd(new Error('foo'));
ssh2Mock.pushResponse({
err: null,
stream: {},
});
const db = new Db(
{
password: '',
},
{
user: 'ubuntu',
password: 'moo',
}
);
await db.connect();
try {
await db.end();
} catch (e) {
expect(e.message).toContain('foo');
}
});
it('should end db.ssh when connection is undefined', async () => {
ssh2Mock.pushResponse({
err: null,
stream: {},
});
const db = new Db(
{
password: '',
},
{
user: 'ubuntu',
password: 'moo',
}
);
await db.connect();
db.connection = undefined;
db.ssh.connection.end = jest.fn();
await db.end();
expect(db.ssh.connection.end).toHaveBeenCalled();
});
});
describe('destroy()', () => {
it('should not error', async () => {
const sql = "UPDATE posts SET status = 'new'";
const mockResults = { affectedRows: 4 };
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.query(sql);
db.destroy();
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
});
it('should allow destroyAll', async () => {
const sql = "UPDATE posts SET status = 'new'";
const mockResults = { affectedRows: 5 };
mysqlMock.pushResponse({ results: mockResults });
const { query, results } = await db.query(sql);
Db.destroyAll();
expect(query).toBe(sql);
expect(results).toEqual(mockResults);
});
it('should destroy when Ssh', async () => {
const stream = {};
ssh2Mock.pushResponse({
err: null,
stream,
});
const db = new Db(
{
password: '',
},
{
user: 'ubuntu',
password: 'moo',
}
);
db.destroy();
});
it('should not error if Db.instances is already empty', async () => {
let err = undefined;
try {
await db.connect();
Db.instances.length = 0;
db.destroy();
} catch (e) {
err = e;
}
expect(err).toBe(undefined);
});
});
describe('escape()', () => {
it('should handle numbers', () => {
expect(db.escape(5)).toBe('5');
});
it('should handle strings', () => {
expect(db.escape('abc')).toBe("'abc'");
});
it('should handle null', () => {
expect(db.escape(null)).toMatch(/^NULL$/i);
});
it('should handle true', () => {
expect(db.escape(true)).toBe('true');
});
it('should handle true', () => {
expect(db.escape(false)).toBe('false');
});
});
describe('escapeQuoteless()', () => {
it('should handle numbers', () => {
expect(db.escapeQuoteless(5)).toBe('5');
});
it('should handle strings', () => {
expect(db.escapeQuoteless('abc')).toBe('abc');
});
it('should handle null', () => {
expect(db.escapeQuoteless(null)).toMatch(/^NULL$/i);
});
it('should handle true', () => {
expect(db.escapeQuoteless(true)).toBe('true');
});
it('should handle false', () => {
expect(db.escapeQuoteless(false)).toBe('false');
});
});
describe('quote()', () => {
it('should add backticks to table or column names', () => {
expect(db.quote('posts')).toBe('`posts`');
});
it('should add backticks to table.column', () => {
expect(db.quote('posts.id')).toBe('`posts`.`id`');
});
it('should properly add backticks to table.*', () => {
expect(db.quote('posts.*')).toBe('`posts`.*');
});
it('should avoid backticks if already present', () => {
expect(db.quote('`invalid')).toBe('`invalid');
});
it('should avoid backticks on functions', () => {
expect(db.quote('COUNT(*)')).toBe('COUNT(*)');
});
it('should avoid backticks on *', () => {
expect(db.quote('*')).toBe('*');
});
});
describe('withInstance()', () => {
it('should run handler', async () => {
const mockResults = [{ foo: 1 }, { foo: 2 }];
mysqlMock.pushResponse({ results: mockResults });
const { results } = await Db.withInstance(db => {
return db.select('SELECT foo FROM bar');
});
expect(results).toEqual(mockResults);
});
it('should accept mysql config', async () => {
const mysqlConfig = { user: 'root' };
const results = await Db.withInstance(mysqlConfig, db => {
return db;
});
expect(results).toBeInstanceOf(Db);
expect(results.config.user).toBe('root');
});
it('should accept ssh config', async () => {
const mysqlConfig = {};
const sshConfig = { user: 'ubuntu' };
const results = await Db.withInstance(mysqlConfig, sshConfig, db => {
return db;
});
expect(results).toBeInstanceOf(Db);
expect(results.ssh.config.user).toBe('ubuntu');
});
it('should call db.end()', async () => {
const spy = jest.fn(() => Promise.resolve(1));
await Db.withInstance(db => {
db.end = spy;
});
expect(spy).toHaveBeenCalled();
});
it('should return Error on handler failure', async () => {
const spy = jest.fn(() => Promise.resolve(1));
const res = await Db.withInstance(db => {
db.end = spy;
throw new Error('foo');
});
expect(spy).toHaveBeenCalled();
expect(res.error).toBeInstanceOf(Error);
});
it('should ignore error on db.end() failure', async () => {
const spy = jest.fn(() => Promise.reject('foobar'));
const res = await Db.withInstance(db => {
db.end = spy;
return 17;
});
expect(spy).toHaveBeenCalled();
expect(res).toBe(17);
});
});
});