From 6b4ddbd8083e2cdfe6d23b5662e312f322147f0d Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Wed, 8 May 2019 00:27:36 +0300 Subject: [PATCH] Initial commit --- package.json | 17 +++ select-builder-oracle.js | 235 ++++++++++++++++++++++++++++++++++++++ select-builder-pgsql.js | 237 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 489 insertions(+) create mode 100644 package.json create mode 100644 select-builder-oracle.js create mode 100644 select-builder-pgsql.js diff --git a/package.json b/package.json new file mode 100644 index 0000000..bcce9f0 --- /dev/null +++ b/package.json @@ -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": { + } +} diff --git a/select-builder-oracle.js b/select-builder-oracle.js new file mode 100644 index 0000000..831a78a --- /dev/null +++ b/select-builder-oracle.js @@ -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, +}; diff --git a/select-builder-pgsql.js b/select-builder-pgsql.js new file mode 100644 index 0000000..983001f --- /dev/null +++ b/select-builder-pgsql.js @@ -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, +};