You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

453 lines
18 KiB

  1. module.exports = {
  2. insert: async function (options) {
  3. const connection = this.parseRequired(options.connection, 'string', 'dbconnector.insert: connection is required.');
  4. const sql = this.parseSQL(options.sql);
  5. const db = this.getDbConnection(connection);
  6. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  7. if (!sql) throw new Error('dbconnector.insert: sql is required.');
  8. if (!sql.table) throw new Error('dbconnector.insert: sql.table is required.');
  9. sql.type = 'insert';
  10. if (db.client == 'couchdb') {
  11. const doc = {};
  12. for (const value of sql.values) {
  13. doc[value.column] = value.value;
  14. }
  15. const result = await db.insert(doc, sql.table + '/' + Date.now());
  16. if (result.ok) {
  17. return { affected: 1, identity: result.id };
  18. } else {
  19. //throw new Error('dbconnector.insert: error inserting document into couchdb.');
  20. return { affected: 0 };
  21. }
  22. }
  23. if (options.test) {
  24. return {
  25. options: options,
  26. query: sql.toString()
  27. };
  28. }
  29. if (sql.sub) {
  30. return db.transaction(async trx => {
  31. // TODO: test how identity is returned for each database
  32. // main insert, returns inserted id
  33. const [identity] = (await trx.fromJSON(sql)).map(value => value[sql.returning] || value);
  34. // loop sub (relation table)
  35. for (let { table, key, value, values } of Object.values(sql.sub)) {
  36. if (!Array.isArray(value)) break;
  37. for (const current of value) {
  38. if (typeof current == 'object') {
  39. current[key] = identity;
  40. await trx(table).insert(current);
  41. } else {
  42. if (values.length != 1) throw new Error('Invalid value mapping');
  43. await trx(table).insert({
  44. [key]: identity,
  45. [values[0].column]: current
  46. });
  47. }
  48. }
  49. }
  50. return { affected: 1, identity };
  51. });
  52. }
  53. let identity = await db.fromJSON(sql);
  54. if (identity) {
  55. if (Array.isArray(identity)) {
  56. identity = identity[0];
  57. }
  58. if (typeof identity == 'object') {
  59. identity = identity[Object.keys(identity)[0]];
  60. }
  61. }
  62. return { affected: 1, identity };
  63. },
  64. update: async function (options) {
  65. const connection = this.parseRequired(options.connection, 'string', 'dbconnector.update: connection is required.');
  66. const sql = this.parseSQL(options.sql);
  67. const db = this.getDbConnection(connection);
  68. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  69. if (!sql) throw new Error('dbconnector.update: sql is required.');
  70. if (!sql.table) throw new Error('dbconnector.update: sql.table is required.');
  71. sql.type = 'update';
  72. if (db.client == 'couchdb') {
  73. let { rows } = await db.list({ include_docs: true, startkey: sql.table + '/', endkey: sql.table + '0' });
  74. rows = rows.map(row => row.doc);
  75. if (sql.wheres) {
  76. const validate = (row, rule) => {
  77. if (rule.operator) {
  78. let a = row[rule.data.column];
  79. let b = rule.value;
  80. switch (rule.operator) {
  81. case 'equal': return a == b;
  82. case 'not_equal': return a != b;
  83. case 'in': return b.includes(a);
  84. case 'not_in': return !b.includes(a);
  85. case 'less': return a < b;
  86. case 'less_or_equal': return a <= b;
  87. case 'greater': return a > b;
  88. case 'greater_or_equal': return a >= b;
  89. case 'between': return b[0] <= a <= b[1];
  90. case 'not_between': return !(b[0] <= a <= b[1]);
  91. case 'begins_with': return String(a).startsWith(String(b));
  92. case 'not_begins_with': return !String(a).startsWith(String(b));
  93. case 'contains': return String(a).includes(String(b));
  94. case 'not_contains': return !String(a).includes(String(b));
  95. case 'ends_with': return String(a).endsWith(String(b));
  96. case 'not_ends_with': return !String(a).endsWith(String(b));
  97. case 'is_empty': return a == null || a == '';
  98. case 'is_not_empty': return a != null && a != '';
  99. case 'is_null': return a == null;
  100. case 'is_not_null': return a != null;
  101. }
  102. }
  103. if (rule.condition && rule.rules.length) {
  104. for (const _rule of rule.rules) {
  105. const valid = validate(row, _rule);
  106. if (!valid && rule.condition == 'AND') return false;
  107. if (valid && rule.condition == 'OR') return true;
  108. }
  109. return rule.condition == 'OR' ? false : true;
  110. }
  111. return true;
  112. };
  113. rows = rows.filter(row => {
  114. return validate(row, sql.wheres);
  115. });
  116. }
  117. let result = []
  118. if (rows.length) {
  119. result = await db.bulk({
  120. docs: rows.map(doc => {
  121. for (const value of sql.values) {
  122. doc[value.column] = value.value;
  123. }
  124. return doc;
  125. })
  126. });
  127. }
  128. return { affected: Array.isArray(result) ? result.filter(result => result.ok).length : rows.length };
  129. }
  130. if (options.test) {
  131. return {
  132. options: options,
  133. query: sql.toString()
  134. };
  135. }
  136. if (sql.sub) {
  137. return db.transaction(async trx => {
  138. let updated = await trx.fromJSON(sql);
  139. if (!Array.isArray(updated)) {
  140. // check if is single update
  141. const single = (
  142. sql.wheres &&
  143. sql.wheres.rules &&
  144. sql.wheres.rules.length == 1 &&
  145. sql.wheres.rules[0].field == sql.returning &&
  146. sql.wheres.rules[0].operation == '='
  147. );
  148. if (single) {
  149. // get id from where condition
  150. updated = [sql.wheres.rules[0].value];
  151. } else {
  152. // create a select with same where conditions
  153. updated = await trx.fromJSON({
  154. ...sql,
  155. type: 'select',
  156. columns: [sql.returning]
  157. });
  158. updated = updated.map(value => value[sql.returning]);
  159. }
  160. } else {
  161. updated = updated.map(value => value[sql.returning]);
  162. }
  163. // loop sub
  164. for (let { table, key, value, values } of Object.values(sql.sub)) {
  165. if (!Array.isArray(value)) continue;
  166. // delete old related data first
  167. await trx(table).whereIn(key, updated).del();
  168. // for each updated item
  169. for (const identity of updated) {
  170. // insert value
  171. for (const current of value) {
  172. if (typeof current == 'object') {
  173. current[key] = identity;
  174. await trx(table).insert(current);
  175. } else {
  176. if (values.length != 1) throw new Error('Invalid value mapping');
  177. await trx(table).insert({
  178. [key]: identity,
  179. [values[0].column]: current
  180. });
  181. }
  182. }
  183. }
  184. }
  185. return { affected: updated.length };
  186. });
  187. }
  188. let affected = await db.fromJSON(sql);
  189. return { affected };
  190. },
  191. delete: async function (options) {
  192. const connection = this.parseRequired(options.connection, 'string', 'dbconnector.delete: connection is required.');
  193. const sql = this.parseSQL(options.sql);
  194. const db = this.getDbConnection(connection);
  195. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  196. if (!sql) throw new Error('dbconnector.delete: sql is required.');
  197. if (!sql.table) throw new Error('dbconnector.delete: sql.table is required.');
  198. sql.type = 'del';
  199. if (db.client == 'couchdb') {
  200. let { rows } = await db.list({ include_docs: true, startkey: sql.table + '/', endkey: sql.table + '0' });
  201. rows = rows.map(row => row.doc);
  202. if (sql.wheres) {
  203. const validate = (row, rule) => {
  204. if (rule.operator) {
  205. let a = row[rule.data.column];
  206. let b = rule.value;
  207. switch (rule.operator) {
  208. case 'equal': return a == b;
  209. case 'not_equal': return a != b;
  210. case 'in': return b.includes(a);
  211. case 'not_in': return !b.includes(a);
  212. case 'less': return a < b;
  213. case 'less_or_equal': return a <= b;
  214. case 'greater': return a > b;
  215. case 'greater_or_equal': return a >= b;
  216. case 'between': return b[0] <= a <= b[1];
  217. case 'not_between': return !(b[0] <= a <= b[1]);
  218. case 'begins_with': return String(a).startsWith(String(b));
  219. case 'not_begins_with': return !String(a).startsWith(String(b));
  220. case 'contains': return String(a).includes(String(b));
  221. case 'not_contains': return !String(a).includes(String(b));
  222. case 'ends_with': return String(a).endsWith(String(b));
  223. case 'not_ends_with': return !String(a).endsWith(String(b));
  224. case 'is_empty': return a == null || a == '';
  225. case 'is_not_empty': return a != null && a != '';
  226. case 'is_null': return a == null;
  227. case 'is_not_null': return a != null;
  228. }
  229. }
  230. if (rule.condition && rule.rules.length) {
  231. for (const _rule of rule.rules) {
  232. const valid = validate(row, _rule);
  233. if (!valid && rule.condition == 'AND') return false;
  234. if (valid && rule.condition == 'OR') return true;
  235. }
  236. return rule.condition == 'OR' ? false : true;
  237. }
  238. return true;
  239. };
  240. rows = rows.filter(row => {
  241. return validate(row, sql.wheres);
  242. });
  243. }
  244. let result = []
  245. if (rows.length) {
  246. result = await db.bulk({
  247. docs: rows.map(doc => {
  248. doc._deleted = true;
  249. return doc;
  250. })
  251. });
  252. }
  253. return { affected: Array.isArray(result) ? result.filter(result => result.ok).length : rows.length };
  254. }
  255. if (options.test) {
  256. return {
  257. options: options,
  258. query: sql.toString()
  259. };
  260. }
  261. if (sql.sub) {
  262. return db.transaction(async trx => {
  263. const deleted = (await trx.fromJSON(sql)).map(value => value[sql.returning] || value);
  264. // loop sub
  265. for (let { table, key } of Object.values(sql.sub)) {
  266. // delete related data
  267. await trx(table).whereIn(key, deleted).del();
  268. }
  269. return { affected: deleted.length };
  270. });
  271. }
  272. let affected = await db.fromJSON(sql);
  273. return { affected };
  274. },
  275. custom: async function (options) {
  276. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.custom: connection is required.');
  277. const sql = this.parseSQL(options.sql);
  278. const db = this.getDbConnection(connection);
  279. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  280. if (!sql) throw new Error('dbconnector.custom: sql is required.');
  281. if (typeof sql.query != 'string') throw new Error('dbupdater.custom: sql.query is required.');
  282. if (!Array.isArray(sql.params)) throw new Error('dbupdater.custom: sql.params is required.');
  283. if (db.client == 'couchdb') {
  284. throw new Error('dbupdater.custom: couchdb is not supported.');
  285. }
  286. const params = [];
  287. const query = sql.query.replace(/([:@][a-zA-Z_]\w*|\?)/g, param => {
  288. if (param == '?') {
  289. params.push(sql.params[params.length].value);
  290. return '?';
  291. }
  292. let p = sql.params.find(p => p.name == param);
  293. if (p) {
  294. params.push(p.value);
  295. return '?';
  296. }
  297. return param;
  298. });
  299. let results = await db.raw(query, params);
  300. if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') {
  301. results = results[0];
  302. } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') {
  303. results = results.rows;
  304. }
  305. return results;
  306. },
  307. execute: async function (options) {
  308. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.execute: connection is required.');
  309. const query = this.parseRequired(options.query, 'string', 'dbupdater.execute: query is required.');
  310. const params = this.parseOptional(options.params, 'object', []);
  311. const db = this.getDbConnection(connection);
  312. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  313. if (db.client == 'couchdb') {
  314. throw new Error('dbupdater.execute: couchdb is not supported.');
  315. }
  316. let results = await db.raw(query, params);
  317. if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') {
  318. results = results[0];
  319. } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') {
  320. results = results.rows;
  321. }
  322. return results;
  323. },
  324. // bulk insert
  325. bulkinsert: async function (options) {
  326. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.bulkinsert: connection is required.');
  327. const sql = this.parseSQL(options.sql);
  328. const source = this.parseRequired(options.source, 'object', 'dbupdater.bulkinsert: source is required.');
  329. const batchsize = this.parseOptional(options.batchsize, 'number', 100);
  330. const db = this.getDbConnection(connection);
  331. if (db.client == 'couchdb') {
  332. throw new Error('dbupdater.bulkinsert: couchdb is not supported.');
  333. }
  334. return await db.transaction(async transaction => {
  335. for (let i = 0; i < source.length; i += batchsize) {
  336. let batch = source.slice(i, i + batchsize).map(data => {
  337. let values = {};
  338. for (let value of sql.values) {
  339. if (value.type == 'json') {
  340. values[value.column] = JSON.stringify(data[value.value]);
  341. } else {
  342. values[value.column] = data[value.value];
  343. }
  344. }
  345. return values;
  346. });
  347. await transaction(sql.table.name || sql.table).insert(batch);
  348. }
  349. return { affected: source.length };
  350. });
  351. },
  352. transaction: async function (options) {
  353. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.transaction: connection is required.');
  354. //const exec = this.parseRequired(options.exec, 'object', 'dbupdater.transaction: exec is required.');
  355. const db = this.getDbConnection(connection);
  356. if (db.client == 'couchdb') {
  357. throw new Error('dbupdater.transaction: couchdb is not supported.');
  358. }
  359. return await db.transaction(async trx => {
  360. this.trx[connection] = trx;
  361. return await this.exec(options.exec, true).finally(() => {
  362. this.trx[connection] = null;
  363. });
  364. });
  365. },
  366. };