From fcbf95ae6aa990d906b4a3ecd5a8308badb2d58e Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Fri, 10 May 2019 01:26:31 +0300 Subject: [PATCH] ES6 + async/await rework seems to work --- ImapManager.js | 17 +- Syncer.js | 61 +++---- SyncerWeb.js | 42 ++--- operetta.js | 21 ++- package.json | 13 +- select-builder-pgsql.js | 351 +++++++++++++++++++++++++++++----------- 6 files changed, 340 insertions(+), 165 deletions(-) diff --git a/ImapManager.js b/ImapManager.js index 98d78cb..e2297ee 100644 --- a/ImapManager.js +++ b/ImapManager.js @@ -1,7 +1,5 @@ const Imap = require('imap'); -module.exports = ImapManager; - class ImapManager { constructor() @@ -29,7 +27,10 @@ class ImapManager if (this.busy[connKey]) { // wait for the queue to finish - await this.queue[connKey](); + await new Promise((r, e) => + { + this.queue[connKey].push(r); + }); } if (stoppingIdle && this.onStopIdle[connKey]) { @@ -46,7 +47,7 @@ class ImapManager return this.connections[connKey]; } - let srv = new Imap(self.accounts[accountId]); + let srv = new Imap(this.accounts[accountId]); // FIXME handle connection errors await new Promise((r, e) => { @@ -56,10 +57,10 @@ class ImapManager await new Promise((r, e) => srv._enqueue('ENABLE QRESYNC', r)); // Monkey-patch node-imap to support VANISHED responses - var oldUT = srv._parser._resUntagged; - srv._parser._resUntagged = () => + let oldUT = srv._parser._resUntagged; + srv._parser._resUntagged = function() { - var m; + let m; if (m = /^\* VANISHED( \(EARLIER\))? ([\d:,]+)/.exec(this._buffer)) { srv.emit('vanish', m[2].split(/,/).map(s => s.split(':'))); @@ -258,3 +259,5 @@ class ImapManager return [ msgrow, attrs ]; } } + +module.exports = ImapManager; diff --git a/Syncer.js b/Syncer.js index c91b9a9..50a5306 100644 --- a/Syncer.js +++ b/Syncer.js @@ -1,11 +1,11 @@ const Imap = require('imap'); -const ImapManager = require('./ImapManager.js'); const EventEmitter = require('events').EventEmitter; const MailParser = require('mailparser').MailParser; const iconv = require('iconv-lite'); const mimelib = require('mimelib'); -module.exports = Syncer; +const ImapManager = require('./ImapManager.js'); +const SQL = require('./select-builder-pgsql.js'); class Syncer { @@ -45,7 +45,7 @@ class Syncer if (row) { await SQL.update(this.pg, 'accounts', { - settings: { imap: account.imap, folders: account.folders } + settings: JSON.stringify({ imap: account.imap, folders: account.folders }) }, { id: row.id }); } else @@ -53,11 +53,11 @@ class Syncer row = (await SQL.insert('accounts', { name: account.name, email: account.email, - settings: { + settings: JSON.stringify({ imap: account.imap, folders: account.folders - } - }, '*'))[0]; + }) + }, { returning: '*' }))[0]; } return row.id; } @@ -91,7 +91,7 @@ class Syncer let srv = await this.getSyncConnection(accountId); await this.syncBox(srv, accountId, 'INBOX'); this.releaseSyncConnection(accountId); - })().catch(console.error); + })().catch(e => console.error(e.stack)); } idleVanish(accountId, uids) @@ -103,7 +103,7 @@ class Syncer this.pg, 'folders', 'id', { name: 'INBOX', account_id: accountId }, null, SQL.MS_VALUE ); await this.deleteVanished(boxId, uids); - })().catch(console.error); + })().catch(e => console.error(e.stack)); } idleExpunge(accountId, seqno) @@ -114,7 +114,7 @@ class Syncer let srv = await this.getSyncConnection(accountId); await this.syncBox(srv, accountId, 'INBOX'); this.releaseSyncConnection(accountId); - })().catch(console.error); + })().catch(e => console.error(e.stack)); } runIdle(accountId, srv) @@ -151,15 +151,15 @@ class Syncer async syncAccount(account) { let accountId = await SQL.select(this.pg, 'accounts', 'id', { email: account.email }, null, SQL.MS_VALUE); - if (accountId) + if (!accountId) { let row = (await SQL.insert(this.pg, 'accounts', { name: account.name, email: account.email, - settings: { + settings: JSON.stringify({ imap: account.imap - } - }, 'id'))[0]; + }) + }, { returning: 'id' }))[0]; accountId = row.id; } let srv = await this.getSyncConnection(accountId); @@ -174,7 +174,7 @@ class Syncer async syncBox(srv, accountId, boxName, boxKind, doFull) { - let boxStatus = await new Promise((r, e) => srv.openBox(boxName, true, r)); + let boxStatus = await new Promise((r, e) => srv.openBox(boxName, true, (err, info) => err ? e(err) : r(info))); // IMAP sync: http://tools.ietf.org/html/rfc4549 let boxRow = await SQL.select(this.pg, 'folders', '*', { account_id: accountId, name: boxStatus.name }, null, SQL.MS_ROW); @@ -194,7 +194,7 @@ class Syncer account_id: accountId, highestmodseq: 0, kind: boxKind||'' - }, '*'))[0]; + }, { returning: '*' }))[0]; } // fetch new messages @@ -220,7 +220,7 @@ class Syncer size: true, bodies: 'HEADER', struct: true, - }, (messages, state) => this.saveMessages(messages, boxRow.id, state)); + }, async (messages, state) => await this.saveMessages(messages, boxRow.id, state)); await SQL.update(this.pg, 'folders', { uidvalidity: boxStatus.uidvalidity, @@ -238,7 +238,7 @@ class Syncer process.stderr.write('\rsynchronizing 0'); await this.imap.runFetch( srv, '1:'+maxUid, {}, - (messages, state) => this.queueFlags(messages, boxId, state), + async (messages, state) => this.queueFlags(messages, boxId, state), { flags: flags, updateFlags: updateFlags, missing: missing||[], total: total, nopause: true } ); process.stderr.write('\n'); @@ -283,8 +283,8 @@ class Syncer { let updated = await SQL.update( this.pg, { m: 'messages', t: SQL.values(updateFlags) }, - { 'flags = t.flags::varchar(255)[]' }, - { 'm.folder_id': boxId, 'm.uid=t.uid': [] }, + [ 'flags = t.flags::varchar(255)[]' ], + { 'm.folder_id': boxId, 'm.uid = t.uid::int': [] }, checkMissing ? { returning: 'm.uid' } : null ); if (checkMissing) @@ -312,7 +312,7 @@ class Syncer srv.on('vanish', onVanish); await this.imap.runFetch( srv, '1:'+maxUid, { modifiers: { changedsince: changedSince+' VANISHED' } }, - (messages, state) => this.queueQuickFlags(messages, boxId, state), { updateFlags: updateFlags } + async (messages, state) => this.queueQuickFlags(messages, boxId, state), { updateFlags: updateFlags } ); srv.removeListener('vanish', onVanish); let checkedMissing = await this.updateFlags(boxId, updateFlags, missing && true); @@ -333,16 +333,22 @@ class Syncer for (let i = 0; i < vanished.length; i++) { if (vanished[i][1]) - dia.push('uid >= '+vanished[i][0]+' AND uid <= '+vanished[i][1]); + { + if (Number(vanished[i][1]) > Number(vanished[i][0]) + 1) + dia.push('uid >= '+vanished[i][0]+' AND uid <= '+vanished[i][1]); + else + lst.push(vanished[i][0], vanished[i][1]); + } else lst.push(vanished[i][0]); } if (lst.length) + { dia.push('uid IN ('+lst.join(',')+')'); - await this.deleteMessages({ folder_id: boxId, '('+dia.join(' OR ')+')': [] }); + } + await this.deleteMessages({ folder_id: boxId, ['('+dia.join(' OR ')+')']: [] }); } - // FIXME: async queueQuickFlags(messages, boxId, fetchState) { for (let i = 0; i < messages.length; i++) @@ -354,16 +360,15 @@ class Syncer async deleteMessages(where) { + let q = SQL.select_builder('messages', 'id', where); await SQL.update( this.pg, 'threads', { first_msg: null }, - { 'first_msg IN ('+SQL.select_builder('messages', 'id', where)+')': [] }, + { ['first_msg IN ('+q.sql+')']: q.bind }, ); await SQL.delete(this.pg, 'messages', where); await SQL.update( this.pg, 'threads', - { ['first_msg=('+SQL.select_builder( - 'messages', 'id', { 'thread_id=threads.id': [] }, { order_by: 'time', limit: 1 } - )+')']: [] }, + [ 'first_msg=(select id from messages where thread_id=threads.id order by time limit 1)' ], { first_msg: null } ); await SQL.delete(this.pg, 'threads', { first_msg: null }); @@ -523,3 +528,5 @@ function toPgArray(a) a = JSON.stringify(a); return '{'+a.substring(1, a.length-1)+'}'; } + +module.exports = Syncer; diff --git a/SyncerWeb.js b/SyncerWeb.js index b9eb058..515972c 100644 --- a/SyncerWeb.js +++ b/SyncerWeb.js @@ -14,8 +14,6 @@ const SQL = require('./select-builder-pgsql.js'); const MAX_FETCH = 100; -module.exports = SyncerWeb; - class SyncerWeb { constructor(syncer, pg, cfg) @@ -32,14 +30,14 @@ class SyncerWeb resave: false, saveUninitialized: false })); - this.app.get('/auth', this.get_auth); - this.app.post('/auth', this.post_auth); - this.app.get('/folders', wrapAsync(this.get_folders)); - this.app.get('/groups', wrapAsync(this.get_groups)); - this.app.get('/messages', wrapAsync(this.get_messages)); - this.app.get('/message', wrapAsync(this.get_message)); - this.app.post('/sync', wrapAsync(this.post_sync)); - this.syncer.events.on('sync', this.syncer_sync); + this.app.get('/auth', (req, res) => this.get_auth(req, res)); + this.app.post('/auth', (req, res) => this.post_auth(req, res)); + this.app.get('/folders', wrapAsync(this, 'get_folders')); + this.app.get('/groups', wrapAsync(this, 'get_groups')); + this.app.get('/messages', wrapAsync(this, 'get_messages')); + this.app.get('/message', wrapAsync(this, 'get_message')); + this.app.post('/sync', wrapAsync(this, 'post_sync')); + this.syncer.events.on('sync', (params) => this.syncer_sync(params)); } listen(port) @@ -47,7 +45,7 @@ class SyncerWeb this.http.listen(port); } - get_auth = (req, res) => + get_auth(req, res) { return res.type('html').send( '
'+ @@ -55,7 +53,7 @@ class SyncerWeb ); } - post_auth = (req, res) => + post_auth(req, res) { if (!req.body) return res.sendStatus(400); @@ -67,7 +65,7 @@ class SyncerWeb return res.send({ ok: false }); } - get_folders = async (req, res) => + async get_folders(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) { @@ -150,7 +148,7 @@ class SyncerWeb return Object.keys(p).length ? p : null; } - get_groups = async (req, res) => + async get_groups(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) { @@ -204,7 +202,7 @@ class SyncerWeb return res.send({ groups: groups }); } - get_messages = async (req, res) => + async get_messages(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) { @@ -232,7 +230,7 @@ class SyncerWeb return res.send({ messages: msgs }); } - get_message = async (req, res) => + async get_message(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) { @@ -264,12 +262,12 @@ class SyncerWeb return res.send({ msg: msg }); } - syncer_sync = (params) => + syncer_sync(params) { this.io.emit('sync', params); } - post_sync = async (req, res) => + async post_sync(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) { @@ -283,7 +281,7 @@ class SyncerWeb return res.send({ status: 'started' }); } - getBody = async (messages, boxId) => + async getBody(messages, boxId) { for (let i = 0; i < messages.length; i++) { @@ -361,9 +359,9 @@ function sanitizeHtml(html) return html; } -function wrapAsync(fn) +function wrapAsync(self, fn) { - return (req, res) => fn(req, res).catch(e => res.status(500).send('Internal Error: '+e.stack)); + return (req, res) => self[fn](req, res).catch(e => res.status(500).send('Internal Error: '+e.stack)); } function ymd(dt) @@ -372,3 +370,5 @@ function ymd(dt) let d = dt.getDate(); return dt.getFullYear()+'-'+(m < 10 ? '0'+m : m)+'-'+(d < 10 ? '0'+d : d); } + +module.exports = SyncerWeb; diff --git a/operetta.js b/operetta.js index 2a4d884..e14cd17 100644 --- a/operetta.js +++ b/operetta.js @@ -46,21 +46,26 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +if (process.env.DEBUG) +{ + global.Promise = require('bluebird'); + global.Promise.config({ longStackTraces: true }); +} + const pg = require('pg'); const Syncer = require('./Syncer.js'); const SyncerWeb = require('./SyncerWeb.js'); -let syncer = new Syncer(pg); -let syncerweb = new SyncerWeb(syncer, pg, cfg); - async function startSync(cfg) { - let connection = new pg.Client(cfg.pg); - await connection.connect(); - await syncer.init(cfg, connection); + let dbh = new pg.Client(cfg.pg); + await dbh.connect(); + let syncer = new Syncer(dbh); + let syncerweb = new SyncerWeb(syncer, dbh, cfg); + await syncer.init(cfg); await syncer.syncAll(); + syncerweb.listen(8057); } let cfg = require('./cfg.json'); -startSync(cfg).catch(console.error); -syncerweb.listen(8057); +startSync(cfg).catch(e => { console.error(e.stack); }); diff --git a/package.json b/package.json index 094fdfa..e98a2b8 100644 --- a/package.json +++ b/package.json @@ -13,22 +13,23 @@ "express-session": "latest", "htmlawed": "latest", "iconv-lite": "latest", - "imap": "latest", + "imap": "^0.8.19", "mailparser": "git+https://github.com/vitalif/mailparser#master", "mimelib": "git+https://github.com/vitalif/mimelib#master", "multer": "latest", "nodemailer": "latest", - "pg": "latest", - "socket.io": "latest", - }, - "peerDependencies": { + "pg": "^7.10.0", + "pg-escape": "^0.2.0", + "socket.io": "latest" }, + "peerDependencies": {}, "devDependencies": { "babel-cli": "latest", - "babel-register": "latest", "babel-plugin-transform-es2015-block-scoping": "latest", "babel-plugin-transform-es2015-destructuring": "latest", "babel-plugin-transform-object-rest-spread": "latest", + "babel-register": "latest", + "bluebird": "^3.5.4", "eslint": "latest", "eslint-plugin-react": "latest" }, diff --git a/select-builder-pgsql.js b/select-builder-pgsql.js index 042c819..32d033c 100644 --- a/select-builder-pgsql.js +++ b/select-builder-pgsql.js @@ -1,9 +1,10 @@ // Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8 // (c) Виталий Филиппов, 2019 -// Версия 2019-05-08 +// Версия 2019-05-09 // В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи, // благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ] +// Здесь так нельзя, поэтому этот синтаксис мы заменяем на { 't1.a=t2.a': [], 't2.b': [ 1, 2, 3 ] } const pg = require('pg'); @@ -42,64 +43,16 @@ function select_builder(tables, fields, where, options) 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) + const t = tables_builder(tables); + sql += t.sql; + bind.push.apply(bind, t.bind); + where = where_builder(where); + sql += ' WHERE '+(where.sql || '1=1'); + bind.push.apply(bind, where.bind); + if (t.moreWhere) { - 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 - { - sql += ' ' + tables[k][0].toUpperCase() + ' JOIN '; - let t = tables[k][1]; - if (t instanceof Pg_Values) - { - sql += '(VALUES '; - let i = 0; - for (const row of t.rows) - { - sql += (i > 0 ? ', (' : '(') + t.keys.map(() => '$'+(++i)).join(', ')+')'; - bind.push.apply(bind, t.keys.map(k => row[k])); - } - sql += ') AS '+k+'('+t.keys.join(', ')+')'; - } - else - { - sql += t + ' ' + k; - } - const on = whereBuilder(tables[k][2]); - sql += ' 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]); - } + sql += ' AND '+t.moreWhere.sql; + bind.push.apply(bind, t.moreWhere.bind); } options = options||{}; if (options['GROUP BY'] || options.group_by) @@ -122,51 +75,228 @@ function select_builder(tables, fields, where, options) { sql += ' LIMIT '+((options.OFFSET || options.offset) | 0); } - return [ sql, bind ]; + return { sql, bind }; } -function whereOrSetBuilder(fields, where) +function tables_builder(tables) +{ + let sql = '', bind = []; + let moreWhere = null; + let first = true; + if (typeof tables == 'string') + { + sql = tables; + return { sql, bind, moreWhere }; + } + for (const k in tables) + { + let jointype = 'INNER', table = tables[k], conds = null; + if (table instanceof Array) + { + [ jointype, table, conds ] = table; + } + if (!first) + { + sql += ' ' + jointype.toUpperCase() + ' JOIN '; + } + let more_on; + if (table instanceof Pg_Values) + { + sql += '(VALUES '; + let i = 0; + for (const row of table.rows) + { + sql += (i > 0 ? ', (' : '(') + table.keys.map(() => '?').join(', ')+')'; + bind.push.apply(bind, table.keys.map(k => row[k])); + i++; + } + sql += ') AS '+k+'('+table.keys.join(', ')+')'; + } + else if (typeof table == 'object') + { + // Nested join, `k` alias is ignored + let subjoin = tables_builder(table); + if (subjoin.moreWhere) + { + more_on = subjoin.moreWhere; + } + if (Object.keys(table).length > 1) + { + sql += "("+subjoin.sql+")"; + } + else + { + sql += subjoin.sql; + } + bind.push.apply(subjoin.bind); + } + else + { + sql += table + ' ' + k; + } + conds = where_builder(conds); + if (more_on) + { + if (!conds.sql) + conds = more_on; + else + { + conds.sql += ' AND ' + more_on.sql; + conds.bind.push.apply(conds.bind, more_on.bind); + } + } + if (!first) + { + sql += ' ON ' + (conds.sql || '1=1'); + bind.push.apply(bind, conds.bind); + } + else + { + // Бывает удобно указывать WHERE как условие "JOIN" первой таблицы + moreWhere = conds.sql ? conds : null; + first = false; + } + } + return { sql, bind, moreWhere }; +} + +// fields: one of: +// - string: 'a=b AND c=d' +// - array: [ 'a=b', [ 'a=? or b=?', 1, 2 ], [ 'a', [ 1, 2 ] ] ] +// - object: { a: 1, b: [ 1, 2 ], 'a = b': [], '(a, b)': [ [ 1, 2 ], [ 2, 3 ] ], 'c=? or d=?': [ 2, 3 ] } +// - key does not contain '?', value is a scalar or non-empty array => (key IN ...) +// - key does not contain '?', value is an empty array => just (key) +// - key contains '?', value is a scalar or non-empty array => (key) with bind params (...value) +// - key is numeric, then value is treated as in array +function where_or_set(fields, where) { if (typeof fields == 'string') - return [ fields, [] ]; + { + return { sql: fields, bind: [] }; + } const w = [], bind = []; - for (const k in fields) + for (let 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 (/^\d+$/.exec(k)) { if (v instanceof Array) { - w.push(v[0]); - bind.push.apply(bind, v.slice(1)); + k = v[0]; + v = v.slice(1); } else { w.push(v); + continue; } } - else if (v != null || v instanceof Array && v.length) + if (k.indexOf('?') >= 0 || v instanceof Array && v.length == 0) { - v = v instanceof Array ? v : [ v ]; - w.push(v.length == 1 ? k + ' = ?' : k + ' in (' + v.map(() => '?').join(', ') + ')'); + if (!(v instanceof Array)) + { + v = [ v ]; + } + // FIXME: check bind variable count + w.push(k); bind.push.apply(bind, v); + continue; + } + v = v instanceof Array ? v : [ v ]; + if (v.length == 1 && v[0] == null) + { + w.push(where ? k+' is null' : k+' = null'); + } + else + { + if ((v.length > 1 || v[0] instanceof Array) && !where) + { + throw new Error('IN syntax can only be used inside WHERE'); + } + if (v[0] instanceof Array) + { + // (a, b) in (...) + w.push(k + ' in (' + v.map(vi => '('+vi.map(() => '?').join(', ')+')') + ')'); + v.map(vi => bind.push.apply(bind, vi)); + } + else + { + 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 ]; + { + // SET + return { sql: w.join(', '), bind }; + } + // WHERE + return { sql: w.length ? '('+w.join(') and (')+')' : '', bind }; } -function whereBuilder(where) +function where_builder(where) { - return whereOrSetBuilder(where, true); + return where_or_set(where, true); +} + +/** + * Разбивает набор таблиц на основную обновляемую + набор дополнительных + * + * Идея в том, чтобы обрабатывать хотя бы 2 простые ситуации: + * UPDATE table1 INNER JOIN table2 ... + * UPDATE table1 LEFT JOIN table2 ... + */ +function split_using(tables) +{ + if (typeof tables == 'string') + { + return { what: { sql: tables, bind: [] }, using: null, moreWhere: null }; + } + let first = null; + let is_next_inner = true; + let i = 0; + for (let k in tables) + { + let t = tables[k]; + if (i == 0) + { + if (t instanceof Array && typeof(t[1]) != 'string') + { + throw new Error('Can only update/delete from real tables, not sub-select, sub-join or VALUES'); + } + first = k; + } + else if (i == 1) + { + is_next_inner = !(t instanceof Array) || t[0].toLowerCase() == 'inner'; + } + else + { + break; + } + i++; + } + let what, moreWhere; + if (is_next_inner) + { + what = tables_builder({ [first]: tables[first] }); + delete tables[first]; + moreWhere = what.moreWhere; + what.moreWhere = null; + } + else + { + what = tables_builder({ ["_"+first]: tables[first] }); + const cond = '_'+first+'.ctid='+(/^\d+$/.exec(first) ? tables[first] : first)+'.ctid'; + moreWhere = what.moreWhere + ? { sql: what.moreWhere.sql+' AND '+cond, bind: what.moreWhere.bind } + : { sql: cond, bind: [] }; + what.moreWhere = null; + } + return { what, using: Object.keys(tables).length > 0 ? tables : null, moreWhere }; } function _positional(sql) @@ -190,7 +320,7 @@ function _inline(sql, bind) // dbh = node-postgres.Client async function select(dbh, tables, fields, where, options, format) { - let [ sql, bind ] = select_builder(tables, fields, where, options); + let { sql, bind } = select_builder(tables, fields, where, options); //console.log(_inline(sql, bind)); let data = await dbh.query(_positional(sql), bind); if ((format & MS_LIST) || (format & MS_COL)) @@ -223,31 +353,59 @@ async function insert(dbh, table, rows, options) sql += (i > 0 ? ', (' : '(') + keys.map(() => '$'+(++i)).join(', ')+')'; bind.push.apply(bind, keys.map(k => row[k])); } - if (options.returning) + if (options && options.returning) { sql += ' returning '+options.returning; return (await dbh.query(sql, bind)).rows; } - else + return await dbh.query(sql, bind); +} + +async function _delete(dbh, table, where, options) +{ + where = where_builder(where); + const split = split_using(table); + if (split.using) { - return await dbh.query(sql, bind); + split.using = tables_builder(split.using); } + let sql = 'delete from '+split.what.sql+ + (split.using ? ' using '+split.using.sql : '')+ + ' where '+(where.sql || '1=1')+(split.moreWhere ? ' and '+split.moreWhere.sql : ''); + let bind = [ ...split.what.bind, ...where.bind, ...(split.moreWhere ? split.moreWhere.bind : []) ]; + if (options && options.returning) + { + sql += ' returning '+options.returning; + return (await dbh.query(_positional(sql), bind)).rows; + } + return await dbh.query(_positional(sql), bind); } -async function _delete(dbh, table, where) +async function update(dbh, table, set, where, options) { - 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); + set = where_or_set(set, false); + where = where_builder(where); + const split = split_using(table); + if (split.using) + { + split.using = tables_builder(split.using); + } + let sql = 'update '+split.what.sql+' set '+set.sql+ + (split.using ? ' from '+split.using.sql : '')+ + ' where '+(where.sql || '1=1')+(split.moreWhere ? ' and '+split.moreWhere.sql : ''); + let bind = [ + ...split.what.bind, + ...set.bind, + ...(split.using ? split.using.bind : []), + ...where.bind, + ...(split.moreWhere ? split.moreWhere.bind : []) + ]; + if (options && options.returning) + { + sql += ' returning '+options.returning; + return (await dbh.query(_positional(sql), bind)).rows; + } + return await dbh.query(_positional(sql), bind); } function values(rows) @@ -266,6 +424,7 @@ class Pg_Values module.exports = { select_builder, + where_builder, select, insert, delete: _delete,