ES6 + async/await rework seems to work
parent
de1429e73a
commit
fcbf95ae6a
|
@ -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;
|
||||
|
|
61
Syncer.js
61
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;
|
||||
|
|
42
SyncerWeb.js
42
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(
|
||||
'<form action="/auth" method="post"><input name="login" />'+
|
||||
|
@ -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;
|
||||
|
|
21
operetta.js
21
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); });
|
||||
|
|
13
package.json
13
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"
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue