fxsql
Version:
Functional query builder based on fxjs
772 lines (702 loc) • 21.5 kB
JavaScript
import dumper from 'dumper.js';
import {
curry,
deepFlat,
each,
filter,
flat,
go,
groupBy,
indexBy,
isFunction,
isString,
map,
mapC,
minBy,
object,
pipe,
pluck,
reduce,
reject,
tap,
uniq,
uniqueBy,
} from 'fxjs/es';
import mysql from 'mysql';
import Pool from "pg-pool";
import pluralize from 'pluralize';
import load_ljoin from './ljoin.js';
const { dump } = dumper;
const { plural, singular } = pluralize;
export const FxSQL_DEBUG = {
DUMP: false,
LOG: false,
ERROR_WITH_SQL: false,
};
const SymbolColumn = Symbol('COLUMN');
const SymbolTag = Symbol('TAG');
const SymbolInjection = Symbol('INJECTION');
const SymbolDefault = Symbol('DEFAULT');
const wrap_arr = (a) => (Array.isArray(a) ? a : [a]);
const mix = (arr1, arr2) =>
arr1.reduce((res, item, i) => {
res.push(item);
i < arr2.length && res.push(arr2[i]);
return res;
}, []);
const uniq_index_by = curry((f, coll) => indexBy(f, uniqueBy(f, coll)));
const first = (a) => a && a[0];
const last = (a) => a && a[a.length - 1];
const is_plain_object = (obj) =>
!!obj && typeof obj == 'object' && obj.constructor == Object;
const is_column = (f) => !!(f && f[SymbolColumn]);
const is_tag = (f) => !!(f && f[SymbolTag]);
const is_injection = (query) => query == SymbolInjection;
const tag = (f) =>
typeof f == 'function'
? Object.assign(f, { [SymbolTag]: true })
: tag((_) => f);
function BASE({
create_pool,
end_pool,
query_fn,
get_connection = (pool) => pool.connect(),
BEGIN = (client) => client.query('BEGIN'),
COMMIT = async (client) => {
await client.query('COMMIT');
return await client.release();
},
ROLLBACK = async (client) => {
await client.query('ROLLBACK');
return await client.release();
},
reg_q = /\?\?/g,
to_q = () => '??',
escape_dq = (idtf) =>
`"${('' + idtf).replace(/\\/g, '\\\\').replace(/"/g, '""')}"`,
replace_q = (query) => {
if (is_injection(query)) return SymbolInjection;
let i = 0;
query.text = query.text.replace(reg_q, (_) => `$${++i}`);
return query;
},
use_ljoin,
}) {
const add_column = (me) =>
me.column == '*'
? COLUMN(me.as + '.*')
: is_column(me.column)
? COLUMN(
...go(
me.column.originals.concat(pluck('left_key', me.rels)),
map((c) => (isString(c) ? me.as + '.' + c : c)),
uniq,
),
)
: is_tag(me.column)
? CL(me.column)
: tag(SymbolInjection);
const columnize = (v) =>
v == '*'
? '*'
: v.match(/\s*\sas\s\s*/i)
? v
.split(/\s*\sas\s\s*/i)
.map(dq)
.join(' AS ')
: dq(v);
const dq = (str) =>
('' + str)
.split('.')
.map((s) => (s == '*' ? s : escape_dq(s)))
.join('.');
function ASSOCIATE_MODULE(strs, ...tails) {
strs = strs.slice();
strs.push(strs.pop() + '\n');
var [strs2, tails2] = import_module(strs, tails);
const splited = deepFlat(strs.map((str) => str.split('\n')))
.filter((str) => str.match(/^\s*/)[0])
.filter((str) => str.trim());
const min = minBy((str) => str.match(/^\s*/)[0].length, splited) || '';
const a = '\n' + min.match(/^\s*/)[0];
return [strs2.map((str) => str.split(a).join('\n')), tails2];
}
function import_module(strs, tails) {
if (!tails.some((tail) => typeof tail == 'function' && !is_tag(tail)))
return [strs, tails];
var strs2 = [...strs];
var j = 0;
var tails2 = tails.map(function (tail, i) {
if (typeof tail != 'function' || is_tag(tail)) return tail;
var k = i + j++;
var spaces = last(strs2[k].split('\n')).match(/^\s*/)[0];
var [strs3, tails3] = tail();
strs2.splice(
k + 1,
0,
strs3.map((str) => str.replace(/\n/g, '\n' + spaces)),
);
return tails3;
});
return [
deepFlat(strs2)
.filter((str) => str.trim())
.reduce((strs, str, i) => {
if (i == 0) return strs.push(str), strs;
const splited = last(strs).split('\n');
if (!last(splited).trim()) {
splited[splited.length - 1] = str.substr(1);
strs[strs.length - 1] = splited.join('\n');
} else {
strs.push(str);
}
return strs;
}, []),
deepFlat(tails2),
];
}
function ready_sqls(strs, tails) {
const [strs2, tails2] = import_module(strs, tails);
const options = strs2.map((s) =>
s
.replace(/\s*\n/, '')
.split('\n')
.map((s) => {
var depth = s.match(/^\s*/)[0].length,
as = s.trim(),
rel_type;
var prefix = as.substr(0, 2);
if (['- ', '< ', 'x '].includes(prefix)) {
rel_type = prefix.trim();
as = as.substr(1).trim();
return { depth, as, rel_type };
} else if (prefix == 'p ') {
rel_type = as[2];
as = as.substr(3).trim();
return { depth, as, rel_type, is_poly: true };
} else {
return { depth, as };
}
}),
);
go(
tails2,
map((tail) =>
is_tag(tail)
? { query: tail }
: Object.assign({}, tail, { query: tail.query || tag() }),
),
Object.entries,
each(([i, t]) => go(options[i], last, (_) => Object.assign(_, t))),
);
return options;
}
function merge_query(queries, sep = ' ') {
if (queries.find(is_injection)) return SymbolInjection;
var query = reduce(
(res, query) => {
if (!query) return res;
if (query.text) res.text += sep + query.text;
if (query.values) res.values.push(...query.values);
return res;
},
{
text: '',
values: [],
},
queries,
);
query.text = query.text.replace(/\n/g, ' ').replace(/\s\s*/g, ' ').trim();
return query;
}
function VALUES(values) {
return tag(function () {
values = Array.isArray(values) ? values : [values];
const columns = go(values, map(Object.keys), flat, uniq);
const DEFAULTS = go(
columns,
map((k) => [k, SymbolDefault]),
object,
);
values = values
.map((v) => Object.assign({}, DEFAULTS, v))
.map((v) => Object.values(v));
return {
text: `(${COLUMN(...columns)().text}) VALUES (${values
.map((v) =>
v.map((v) => (v == SymbolDefault ? 'DEFAULT' : to_q())).join(', '),
)
.join('), (')})`,
values: flat(values.map((v) => v.filter((v) => v != SymbolDefault))),
};
});
}
function COLUMN(...originals) {
return Object.assign(
tag(function () {
let sqls = flat(
originals.map((v) =>
isString(v)
? [{ text: columnize(v) }, { text: ', ' }]
: is_tag(v)
? [v(), { text: ', ' }]
: [
{
text: Object.entries(v)
.map((v) => v.map(dq).join(' AS '))
.join(', '),
},
{ text: ', ' },
],
),
);
sqls.pop();
return merge_query(sqls, '');
}),
{ [SymbolColumn]: true, originals: originals },
);
}
const CL = COLUMN,
TABLE = COLUMN,
TB = TABLE;
function PARAMS(obj, sep) {
return tag(function () {
let i = 0;
const text = Object.keys(obj)
.map((k) => `${columnize(k)} = ${to_q()}`)
.join(sep);
const values = Object.values(obj);
return {
text: text.replace(reg_q, function () {
const value = values[i++];
return is_column(value) ? value().text : to_q();
}),
values: reject(is_column, values),
};
});
}
function EQ(obj, sep = 'AND') {
return PARAMS(obj, ' ' + sep + ' ');
}
function SET(obj) {
return tag(function () {
const query = PARAMS(obj, ', ')();
query.text = 'SET ' + query.text;
return query;
});
}
function BASE_IN(key, operator, values) {
values = uniq(values);
var keys_text = COLUMN(...wrap_arr(key))().text;
return {
text: `${
Array.isArray(key) ? `(${keys_text})` : keys_text
} ${operator} (${values
.map(Array.isArray(key) ? (v) => `(${v.map(to_q).join(', ')})` : to_q)
.join(', ')})`,
values: deepFlat(values),
};
}
function IN(key, values) {
return tag(function () {
if (!values || !values.length) return { text: `1=??`, values: [0] };
return BASE_IN(key, 'IN', values);
});
}
function NOT_IN(key, values) {
return tag(function () {
if (!values || !values.length) return { text: `1=??`, values: [0] };
return BASE_IN(key, 'NOT IN', values);
});
}
function _SQL(texts, values) {
return go(
mix(
texts.map((text) => ({ text })),
values.map((value) =>
is_tag(value)
? value()
: isFunction(value)
? SymbolInjection
: { text: to_q(), values: [value] },
),
),
merge_query,
);
}
function SQL(texts, ...values) {
return tag(function () {
return _SQL(texts, values);
});
}
function SQLS(sqls) {
return tag(function () {
return sqls.find((sql) => !is_tag(sql))
? SymbolInjection
: merge_query(sqls.map((sql) => sql()));
});
}
function baseAssociate(QUERY) {
return async function (strs, ...tails) {
return go(
ready_sqls(strs, tails),
deepFlat,
filter((t) => t.as),
each((option) => {
option.column = option.column || '*';
option.join = option.join || SQL``;
option.query = option.query || tag();
option.table =
option.table ||
(option.rel_type == '-' ? plural(option.as) : option.as);
option.rels = [];
option.row_number = option.row_number || [];
}),
function setting([left, ...rest]) {
const cur = [left];
each((me) => {
while (!(last(cur).depth < me.depth)) cur.pop();
const left = last(cur);
left.rels.push(me);
if (me.rel_type == '-') {
me.left_key =
me.left_key || (me.is_poly ? 'id' : singular(me.table) + '_id');
me.where_key = me.key || (me.is_poly ? 'attached_id' : 'id');
me.xjoin = tag();
} else if (me.rel_type == '<') {
me.left_key = me.left_key || 'id';
me.where_key =
me.key ||
(me.is_poly ? 'attached_id' : singular(left.table) + '_id');
me.xjoin = tag();
} else if (me.rel_type == 'x') {
me.left_key = me.left_key || 'id';
const xtable = me.xtable || left.table + '_' + me.table;
const xtable_as = me.xtable_as || xtable;
me.where_key = `${xtable_as}.${
me.left_xkey || singular(left.table) + '_id'
}`;
me.xjoin = SQL`INNER JOIN ${TB(xtable)} AS ${TB(
xtable_as,
)} ON ${EQ({
[`${xtable_as}.${
me.xkey || singular(me.table) + '_id'
}`]: COLUMN(me.as + '.' + (me.key || 'id')),
})}`;
}
me.poly_type = me.is_poly
? SQL`AND ${EQ(
is_plain_object(me.poly_type)
? me.poly_type
: { attached_type: me.poly_type || left.table },
)}`
: tag();
cur.push(me);
}, rest);
return left;
},
async function (me) {
const lefts = await QUERY`
SELECT ${add_column(me)}
FROM ${TB(me.table)} AS ${TB(me.as)} ${me.query}`;
return go(
[lefts, me],
function recur([lefts, option]) {
return (
lefts.length &&
option.rels.length &&
go(
option.rels,
mapC(async function (me) {
const query = me.query();
if (query && query.text)
query.text = query.text.replace(/^\s*WHERE/i, 'AND');
var fold_key =
me.rel_type == 'x'
? `_#_${me.where_key.split('.')[1]}_#_`
: me.where_key;
const colums = uniq(
add_column(me).originals.concat(
me.rel_type != 'x'
? me.as + '.' + me.where_key
: me.where_key + ' AS ' + fold_key,
),
);
const in_vals = filter(
(a) => a != null,
pluck(me.left_key, lefts),
);
const is_row_num = me.row_number.length == 2;
const from_sql = SQL`
FROM ${TB(me.table)} AS ${TB(me.as)}
${me.join}
${me.xjoin}
WHERE
${IN(
(me.rel_type == 'x' ? '' : me.as + '.') + me.where_key,
in_vals,
)}
${me.poly_type}
${tag(query)}`;
const rights = !in_vals.length
? []
: await (is_row_num
? QUERY`
SELECT *
FROM (
SELECT
${COLUMN(...colums)},
ROW_NUMBER() OVER (PARTITION BY ${CL(
me.where_key,
)} ORDER BY ${me.row_number[1]}) as "--row_number--"
${from_sql}
) AS "--row_number_table--"
WHERE "--row_number_table--"."--row_number--"<=${
me.row_number[0]
}`
: QUERY`SELECT ${COLUMN(...colums)} ${from_sql}`);
const [folder, default_value] =
me.rel_type == '-'
? [uniq_index_by, () => ({})]
: [groupBy, () => []];
return go(
rights,
is_row_num
? map((r) => delete r['--row_number--'] && r)
: (r) => r,
folder((a) => a[fold_key]),
(folded) =>
each(function (left) {
left._ = left._ || {};
left._[me.as] =
folded[left[me.left_key]] || default_value();
if (me.rel_type == 'x')
each((a) => delete a[fold_key], left._[me.as]);
}, lefts),
(_) => recur([rights, me]),
(_) =>
me.hook &&
each(
(left) =>
go(
me.hook(left._[me.as]),
(right) => (left._[me.as] = right),
),
lefts,
),
);
}),
)
);
},
(_) => (me.hook ? me.hook(lefts) : lefts),
);
},
);
};
}
function CONNECT(connection_info) {
const pool = create_pool(connection_info);
const pool_query = query_fn(pool);
const _on2_obj = {
error: function () {},
};
pool.queryError = (cb) => {
_on2_obj['error'] = cb;
};
async function base_query(excute_query, texts, values, transaction_querys) {
const error_for_stack = new Error();
try {
var query = replace_q(_SQL(texts, values));
if (Array.isArray(transaction_querys))
transaction_querys.push({
text: query.text,
values: JSON.stringify(query.values),
stack: FxSQL_DEBUG.LOG && new Error().stack,
});
return await go(
is_injection(query) ? Promise.reject('INJECTION ERROR') : query,
tap(function (query) {
if (FxSQL_DEBUG.DUMP) dump(query);
typeof FxSQL_DEBUG.LOG == 'function'
? FxSQL_DEBUG.LOG(query)
: FxSQL_DEBUG.LOG && console.log(query.text, '\n', query.values);
}),
excute_query,
);
} catch (e) {
FxSQL_DEBUG.ERROR_WITH_SQL &&
(e.stack = `\nFxSQL_DEBUG.ERROR_WITH_SQL:\n text: ${
query.text
}\n values: ${JSON.stringify(query.values)}\n${e.stack}`);
error_for_stack.message = e.message;
_on2_obj.error(query, error_for_stack);
throw e;
}
}
function QUERY(texts, ...values) {
return base_query(pool_query, texts, values);
}
function END() {
return end_pool(pool);
}
const QUERY1 = pipe(QUERY, first),
ASSOCIATE = baseAssociate(QUERY),
ASSOCIATE1 = pipe(ASSOCIATE, first);
var ljoin = null;
async function LOAD_LJOIN(QUERY) {
if (!ljoin)
ljoin = await load_ljoin({
ready_sqls,
add_column,
tag,
FxSQL_DEBUG,
connection_info,
QUERY,
VALUES,
IN,
NOT_IN,
EQ,
SET,
COLUMN,
CL,
TABLE,
TB,
SQL,
SQLS,
});
return ljoin(QUERY);
}
let baseTransactionQuery = function () {};
let transactionErrorHandler = function (err) {
throw err;
};
return {
POOL: pool,
VALUES,
IN,
NOT_IN,
EQ,
SET,
COLUMN,
CL,
TABLE,
TB,
SQL,
SQLS,
FxSQL_DEBUG,
QUERY,
QUERY1,
ASSOCIATE,
ASSOCIATE1,
ASSOCIATE_MODULE,
END,
LOAD_LJOIN: use_ljoin ? LOAD_LJOIN : null,
config: {
setBaseTransactionQuery(func) {
baseTransactionQuery = func;
},
setTransactionErrorHandler(func) {
transactionErrorHandler = func;
},
},
async TRANSACTION() {
const stack = Error('Transaction start stacktrace');
try {
const client = await get_connection(pool);
const client_query = query_fn(client);
const transaction_querys = [];
await BEGIN(client);
const QUERY = function QUERY(texts, ...values) {
return base_query(client_query, texts, values, transaction_querys);
};
const QUERY1 = pipe(QUERY, first),
ASSOCIATE = baseAssociate(QUERY),
ASSOCIATE1 = pipe(ASSOCIATE, first);
await baseTransactionQuery(QUERY, QUERY1);
client.on('error', (err) => {
transactionErrorHandler(err, client, transaction_querys, stack);
});
return {
client,
VALUES,
IN,
NOT_IN,
EQ,
SET,
COLUMN,
CL,
TABLE,
TB,
SQL,
QUERY,
QUERY1,
ASSOCIATE,
ASSOCIATE1,
LJOIN: use_ljoin && ljoin ? await ljoin(QUERY) : null,
COMMIT: (_) => {
const { stack } = new Error();
transaction_querys.push({ query: 'COMMIT', VALUES: [], stack });
return COMMIT(client);
},
ROLLBACK: (_) => {
const { stack } = new Error();
transaction_querys.push({ query: 'ROLLBACK', VALUES: [], stack });
return ROLLBACK(client);
},
};
} catch (e) {
throw e;
}
},
};
}
return {
CONNECT,
VALUES,
IN,
NOT_IN,
EQ,
SET,
COLUMN,
CL,
TABLE,
TB,
SQL,
SQLS,
FxSQL_DEBUG,
};
}
const method_promise = curry(
(name, obj) =>
new Promise((resolve, reject) =>
obj[name]((err, res) => (err ? reject(err) : resolve(res))),
),
);
export const PostgreSQL = BASE({
create_pool: (connection_info) => new Pool(connection_info),
end_pool: (pool) => pool.end(),
query_fn: (pool) => pipe(pool.query.bind(pool), (res) => res.rows),
use_ljoin: true,
}),
MySQL = BASE({
create_pool: (connection_info) => mysql.createPool(connection_info),
end_pool: (pool) =>
new Promise((resolve, reject) =>
pool.end((err) => (err ? reject(err) : resolve())),
),
query_fn: (pool) => ({ text, values }) =>
new Promise((resolve, reject) =>
pool.query(text, values, (err, results) =>
err ? reject(err) : resolve(results),
),
),
get_connection: method_promise('getConnection'),
BEGIN: method_promise('beginTransaction'),
COMMIT: method_promise('commit'),
ROLLBACK: method_promise('rollback'),
reg_q: /\?/g,
to_q: () => '?',
escape_dq: mysql.escapeId,
replace_q: (_) => _,
});