Initial commit
commit
6b4ddbd808
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"author": {
|
||||
"name": "Vitaliy Filippov",
|
||||
"email": "vitalif@yourcmc.ru",
|
||||
"url": "http://yourcmc.ru/wiki/"
|
||||
},
|
||||
"name": "mw-select-builder",
|
||||
"description": "MediaWiki-like select builder",
|
||||
"dependencies": {
|
||||
"pg": "^7.9.0",
|
||||
"pg-escape": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
},
|
||||
"scripts": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
// Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8
|
||||
// (c) Виталий Филиппов, 2018
|
||||
|
||||
// В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи,
|
||||
// благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ]
|
||||
|
||||
const MS_HASH = 0;
|
||||
const MS_LIST = 1;
|
||||
const MS_ROW = 2;
|
||||
const MS_COL = 4;
|
||||
const MS_VALUE = 6;
|
||||
|
||||
function limitOffset(sql, limit, offset)
|
||||
{
|
||||
if (!limit && !offset)
|
||||
return sql;
|
||||
const w = [];
|
||||
if (limit)
|
||||
w.push('rownum <= '+((limit|0)+(offset|0)));
|
||||
if (offset)
|
||||
w.push('rownum >= '+(1+(offset|0)));
|
||||
return 'select * from ('+sql+') t where '+w.join(' and ');
|
||||
}
|
||||
|
||||
function hashResult(res)
|
||||
{
|
||||
const keys = res.metaData.map(m => m.name.toLowerCase());
|
||||
const hashes = [];
|
||||
for (const row of res.rows)
|
||||
{
|
||||
const h = {};
|
||||
keys.map((k, i) => h[k] = row[i]);
|
||||
hashes.push(h);
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
function selectBuilder(tables, fields, where, options)
|
||||
{
|
||||
let sql = 'SELECT ', bind = [];
|
||||
if (fields instanceof Array)
|
||||
{
|
||||
sql += fields.join(', ');
|
||||
}
|
||||
else if (typeof fields == 'string')
|
||||
{
|
||||
sql += fields;
|
||||
}
|
||||
else if (typeof fields == 'object')
|
||||
{
|
||||
sql += Object.keys(fields).map(k => fields[k]+' AS '+k).join(', ');
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Error('fields = '+fields+' is invalid');
|
||||
}
|
||||
sql += ' FROM ';
|
||||
let first = true;
|
||||
let moreWhere = null;
|
||||
tables = typeof tables == 'string' ? { t: tables } : tables;
|
||||
for (const k in tables)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
if (typeof tables[k] != 'string')
|
||||
{
|
||||
// Бывает удобно указывать WHERE как условие "JOIN" первой таблицы
|
||||
sql += tables[k][1] + ' ' + k;
|
||||
moreWhere = tables[k][2];
|
||||
}
|
||||
else
|
||||
{
|
||||
sql += tables[k] + ' ' + k;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
else if (typeof tables[k] == 'string')
|
||||
{
|
||||
sql += ' INNER JOIN '+tables[k]+' '+k+' ON 1=1';
|
||||
}
|
||||
else
|
||||
{
|
||||
const on = whereBuilder(tables[k][2]);
|
||||
sql += ' ' + tables[k][0].toUpperCase() + ' JOIN ' + tables[k][1] + ' ' + k + ' ON ' + (on[0] || '1=1');
|
||||
bind.push.apply(bind, on[1]);
|
||||
}
|
||||
}
|
||||
const w = whereBuilder(where);
|
||||
sql += ' WHERE '+(w[0] || '1=1');
|
||||
bind.push.apply(bind, w[1]);
|
||||
if (moreWhere)
|
||||
{
|
||||
moreWhere = whereBuilder(moreWhere);
|
||||
if (moreWhere[0])
|
||||
{
|
||||
sql += ' AND '+moreWhere[0];
|
||||
bind.push.apply(bind, moreWhere[1]);
|
||||
}
|
||||
}
|
||||
options = options||{};
|
||||
if (options['GROUP BY'] || options.group_by)
|
||||
{
|
||||
let group = options['GROUP BY'] || options.group_by;
|
||||
group = group instanceof Array ? group : [ group ];
|
||||
sql += ' GROUP BY '+group.join(', ');
|
||||
}
|
||||
if (options['ORDER BY'] || options.order_by)
|
||||
{
|
||||
let order = options['ORDER BY'] || options.order_by;
|
||||
order = order instanceof Array ? order : [ order ];
|
||||
sql += ' ORDER BY '+order.join(', ');
|
||||
}
|
||||
if (options.LIMIT || options.OFFSET || options.limit || options.offset)
|
||||
{
|
||||
sql = limitOffset(sql, options.LIMIT || options.limit, options.OFFSET || options.offset);
|
||||
}
|
||||
return [ sql, bind ];
|
||||
}
|
||||
|
||||
function whereOrSetBuilder(fields, where)
|
||||
{
|
||||
if (typeof fields == 'string')
|
||||
return [ fields, [] ];
|
||||
const w = [], bind = [];
|
||||
for (const k in fields)
|
||||
{
|
||||
let v = fields[k];
|
||||
if (k.indexOf('?') >= 0)
|
||||
{
|
||||
if (!(v instanceof Array))
|
||||
v = [ v ];
|
||||
w.push(k);
|
||||
bind.push.apply(bind, v);
|
||||
}
|
||||
else if (/^\d+$/.exec(k))
|
||||
{
|
||||
if (v instanceof Array)
|
||||
{
|
||||
w.push(v[0]);
|
||||
bind.push.apply(bind, v.slice(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
w.push(v);
|
||||
}
|
||||
}
|
||||
else if (v != null || v instanceof Array && v.length)
|
||||
{
|
||||
v = v instanceof Array ? v : [ v ];
|
||||
w.push(v.length == 1 ? k + ' = ?' : k + ' in (' + v.map(() => '?').join(', ') + ')');
|
||||
bind.push.apply(bind, v);
|
||||
}
|
||||
}
|
||||
if (!where)
|
||||
return [ w.join(', '), bind ];
|
||||
return [ w.length ? '('+w.join(') and (')+')' : '', bind ];
|
||||
}
|
||||
|
||||
function whereBuilder(where)
|
||||
{
|
||||
return whereOrSetBuilder(where, true);
|
||||
}
|
||||
|
||||
function _positional(sql)
|
||||
{
|
||||
let i = 0;
|
||||
sql = sql.replace(/\?/g, () => ':'+(++i));
|
||||
return sql;
|
||||
}
|
||||
|
||||
async function select(dbh, tables, fields, where, options, format)
|
||||
{
|
||||
let [ sql, bind ] = selectBuilder(tables, fields, where, options);
|
||||
let data = await dbh.execute(_positional(sql), bind);
|
||||
if ((format & MS_LIST) || (format & MS_COL))
|
||||
data = data.rows;
|
||||
else
|
||||
data = hashResult(data);
|
||||
if (format & MS_ROW)
|
||||
data = data[0];
|
||||
if (data && (format & MS_COL))
|
||||
data = data[0];
|
||||
return data;
|
||||
}
|
||||
|
||||
async function insert(dbh, table, rows)
|
||||
{
|
||||
if (!(rows instanceof Array))
|
||||
{
|
||||
rows = [ rows ];
|
||||
}
|
||||
if (!rows.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
const keys = Object.keys(rows[0]);
|
||||
let sql = 'insert into '+table+' ('+keys.join(', ')+') ';
|
||||
const bind = [];
|
||||
let i = 0;
|
||||
for (const row of rows)
|
||||
{
|
||||
sql += (i > 0 ? ' union all ' : '')+' select'+keys.map(() => ':'+(++i)).join(', ')+' from dual';
|
||||
bind.push.apply(bind, keys.map(k => row[k]));
|
||||
}
|
||||
return await dbh.execute(sql, bind);
|
||||
}
|
||||
|
||||
async function _delete(dbh, table, where)
|
||||
{
|
||||
const w = whereBuilder(where);
|
||||
const sql = 'DELETE FROM '+table+' WHERE '+(w[0] || '1=1');
|
||||
return await dbh.execute(_positional(sql), w[1]);
|
||||
}
|
||||
|
||||
async function update(dbh, table, set, where)
|
||||
{
|
||||
set = whereOrSetBuilder(set, false);
|
||||
where = whereOrSetBuilder(where, true)
|
||||
const sql = 'UPDATE '+table+' SET '+set[0]+' WHERE '+(where[0] || '1=1');
|
||||
const bind = [ ...set[1], ...where[1] ];
|
||||
return await dbh.execute(_positional(sql), bind);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
select,
|
||||
insert,
|
||||
delete: _delete,
|
||||
update,
|
||||
MS_HASH,
|
||||
MS_LIST,
|
||||
MS_ROW,
|
||||
MS_COL,
|
||||
MS_VALUE,
|
||||
hashResult,
|
||||
};
|
|
@ -0,0 +1,237 @@
|
|||
// Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8
|
||||
// (c) Виталий Филиппов, 2019
|
||||
|
||||
// В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи,
|
||||
// благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ]
|
||||
|
||||
const pg = require('pg');
|
||||
|
||||
// Сраный node-postgres конвертирует даты в Date и портит таймзону
|
||||
const DATATYPE_DATE = 1082;
|
||||
pg.types.setTypeParser(DATATYPE_DATE, function(val)
|
||||
{
|
||||
return val === null ? null : val;
|
||||
});
|
||||
|
||||
let pg_escape;
|
||||
|
||||
const MS_HASH = 0;
|
||||
const MS_LIST = 1;
|
||||
const MS_ROW = 2;
|
||||
const MS_COL = 4;
|
||||
const MS_VALUE = 6;
|
||||
|
||||
function selectBuilder(tables, fields, where, options)
|
||||
{
|
||||
let sql = 'SELECT ', bind = [];
|
||||
if (fields instanceof Array)
|
||||
{
|
||||
sql += fields.join(', ');
|
||||
}
|
||||
else if (typeof fields == 'string')
|
||||
{
|
||||
sql += fields;
|
||||
}
|
||||
else if (typeof fields == 'object')
|
||||
{
|
||||
sql += Object.keys(fields).map(k => fields[k]+' AS '+k).join(', ');
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Error('fields = '+fields+' is invalid');
|
||||
}
|
||||
sql += ' FROM ';
|
||||
let first = true;
|
||||
let moreWhere = null;
|
||||
tables = typeof tables == 'string' ? { t: tables } : tables;
|
||||
for (const k in tables)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
if (typeof tables[k] != 'string')
|
||||
{
|
||||
// Бывает удобно указывать WHERE как условие "JOIN" первой таблицы
|
||||
sql += tables[k][1] + ' ' + k;
|
||||
moreWhere = tables[k][2];
|
||||
}
|
||||
else
|
||||
{
|
||||
sql += tables[k] + ' ' + k;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
else if (typeof tables[k] == 'string')
|
||||
{
|
||||
sql += ' INNER JOIN '+tables[k]+' '+k+' ON 1=1';
|
||||
}
|
||||
else
|
||||
{
|
||||
const on = whereBuilder(tables[k][2]);
|
||||
sql += ' ' + tables[k][0].toUpperCase() + ' JOIN ' + tables[k][1] + ' ' + k + ' ON ' + (on[0] || '1=1');
|
||||
bind.push.apply(bind, on[1]);
|
||||
}
|
||||
}
|
||||
const w = whereBuilder(where);
|
||||
sql += ' WHERE '+(w[0] || '1=1');
|
||||
bind.push.apply(bind, w[1]);
|
||||
if (moreWhere)
|
||||
{
|
||||
moreWhere = whereBuilder(moreWhere);
|
||||
if (moreWhere[0])
|
||||
{
|
||||
sql += ' AND '+moreWhere[0];
|
||||
bind.push.apply(bind, moreWhere[1]);
|
||||
}
|
||||
}
|
||||
options = options||{};
|
||||
if (options['GROUP BY'] || options.group_by)
|
||||
{
|
||||
let group = options['GROUP BY'] || options.group_by;
|
||||
group = group instanceof Array ? group : [ group ];
|
||||
sql += ' GROUP BY '+group.join(', ');
|
||||
}
|
||||
if (options['ORDER BY'] || options.order_by)
|
||||
{
|
||||
let order = options['ORDER BY'] || options.order_by;
|
||||
order = order instanceof Array ? order : [ order ];
|
||||
sql += ' ORDER BY '+order.join(', ');
|
||||
}
|
||||
if (options.LIMIT || options.limit)
|
||||
{
|
||||
sql += ' LIMIT '+((options.LIMIT || options.limit) | 0);
|
||||
}
|
||||
if (options.OFFSET || options.offset)
|
||||
{
|
||||
sql += ' LIMIT '+((options.OFFSET || options.offset) | 0);
|
||||
}
|
||||
return [ sql, bind ];
|
||||
}
|
||||
|
||||
function whereOrSetBuilder(fields, where)
|
||||
{
|
||||
if (typeof fields == 'string')
|
||||
return [ fields, [] ];
|
||||
const w = [], bind = [];
|
||||
for (const k in fields)
|
||||
{
|
||||
let v = fields[k];
|
||||
if (k.indexOf('?') >= 0)
|
||||
{
|
||||
if (!(v instanceof Array))
|
||||
v = [ v ];
|
||||
w.push(k);
|
||||
bind.push.apply(bind, v);
|
||||
}
|
||||
else if (/^\d+$/.exec(k))
|
||||
{
|
||||
if (v instanceof Array)
|
||||
{
|
||||
w.push(v[0]);
|
||||
bind.push.apply(bind, v.slice(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
w.push(v);
|
||||
}
|
||||
}
|
||||
else if (v != null || v instanceof Array && v.length)
|
||||
{
|
||||
v = v instanceof Array ? v : [ v ];
|
||||
w.push(v.length == 1 ? k + ' = ?' : k + ' in (' + v.map(() => '?').join(', ') + ')');
|
||||
bind.push.apply(bind, v);
|
||||
}
|
||||
}
|
||||
if (!where)
|
||||
return [ w.join(', '), bind ];
|
||||
return [ w.length ? '('+w.join(') and (')+')' : '', bind ];
|
||||
}
|
||||
|
||||
function whereBuilder(where)
|
||||
{
|
||||
return whereOrSetBuilder(where, true);
|
||||
}
|
||||
|
||||
function _positional(sql)
|
||||
{
|
||||
let i = 0;
|
||||
sql = sql.replace(/\?/g, () => '$'+(++i));
|
||||
return sql;
|
||||
}
|
||||
|
||||
function _inline(sql, bind)
|
||||
{
|
||||
if (!pg_escape)
|
||||
{
|
||||
pg_escape = require('pg-escape');
|
||||
}
|
||||
let i = 0;
|
||||
sql = sql.replace(/\?/g, () => '\''+pg_escape.string(bind[i++])+'\'');
|
||||
return sql;
|
||||
}
|
||||
|
||||
// dbh = node-postgres.Client
|
||||
async function select(dbh, tables, fields, where, options, format)
|
||||
{
|
||||
let [ sql, bind ] = selectBuilder(tables, fields, where, options);
|
||||
//console.log(_inline(sql, bind));
|
||||
let data = await dbh.query(_positional(sql), bind);
|
||||
if ((format & MS_LIST) || (format & MS_COL))
|
||||
data = data.rows.map(r => Object.values(r));
|
||||
else
|
||||
data = data.rows;
|
||||
if (format & MS_ROW)
|
||||
data = data[0];
|
||||
if (data && (format & MS_COL))
|
||||
data = data[0];
|
||||
return data;
|
||||
}
|
||||
|
||||
async function insert(dbh, table, rows)
|
||||
{
|
||||
if (!(rows instanceof Array))
|
||||
{
|
||||
rows = [ rows ];
|
||||
}
|
||||
if (!rows.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
const keys = Object.keys(rows[0]);
|
||||
let sql = 'insert into '+table+' ('+keys.join(', ')+') values ';
|
||||
const bind = [];
|
||||
let i = 0;
|
||||
for (const row of rows)
|
||||
{
|
||||
sql += (i > 0 ? ', (' : '(') + keys.map(() => '$'+(++i)).join(', ')+')';
|
||||
bind.push.apply(bind, keys.map(k => row[k]));
|
||||
}
|
||||
return await dbh.query(sql, bind);
|
||||
}
|
||||
|
||||
async function _delete(dbh, table, where)
|
||||
{
|
||||
const w = whereBuilder(where);
|
||||
const sql = 'DELETE FROM '+table+' WHERE '+(w[0] || '1=1');
|
||||
return await dbh.execute(_positional(sql), w[1]);
|
||||
}
|
||||
|
||||
async function update(dbh, table, set, where)
|
||||
{
|
||||
set = whereOrSetBuilder(set, false);
|
||||
where = whereOrSetBuilder(where, true)
|
||||
const sql = 'UPDATE '+table+' SET '+set[0]+' WHERE '+(where[0] || '1=1');
|
||||
const bind = [ ...set[1], ...where[1] ];
|
||||
return await dbh.execute(_positional(sql), bind);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
select,
|
||||
insert,
|
||||
delete: _delete,
|
||||
update,
|
||||
MS_HASH,
|
||||
MS_LIST,
|
||||
MS_ROW,
|
||||
MS_COL,
|
||||
MS_VALUE,
|
||||
};
|
Loading…
Reference in New Issue