Initial commit

master
Vitaliy Filippov 2019-05-08 00:27:36 +03:00
commit 6b4ddbd808
3 changed files with 489 additions and 0 deletions

17
package.json Normal file
View File

@ -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": {
}
}

235
select-builder-oracle.js Normal file
View File

@ -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,
};

237
select-builder-pgsql.js Normal file
View File

@ -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,
};