Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

412 рядки
17 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. const result = await db.bulk(rows.map(doc => {
  118. for (const value of sql.values) {
  119. doc[value.column] = value.value;
  120. }
  121. return doc;
  122. }));
  123. return { affected: Array.isArray(result) ? result.filter(result => result.ok).length : rows.length };
  124. }
  125. if (options.test) {
  126. return {
  127. options: options,
  128. query: sql.toString()
  129. };
  130. }
  131. if (sql.sub) {
  132. return db.transaction(async trx => {
  133. let updated = await trx.fromJSON(sql);
  134. if (!Array.isArray(updated)) {
  135. // check if is single update
  136. const single = (
  137. sql.wheres &&
  138. sql.wheres.rules &&
  139. sql.wheres.rules.length == 1 &&
  140. sql.wheres.rules[0].field == sql.returning &&
  141. sql.wheres.rules[0].operation == '='
  142. );
  143. if (single) {
  144. // get id from where condition
  145. updated = [sql.wheres.rules[0].value];
  146. } else {
  147. // create a select with same where conditions
  148. updated = await trx.fromJSON({
  149. ...sql,
  150. type: 'select',
  151. columns: [sql.returning]
  152. });
  153. updated = updated.map(value => value[sql.returning]);
  154. }
  155. } else {
  156. updated = updated.map(value => value[sql.returning]);
  157. }
  158. // loop sub
  159. for (let { table, key, value, values } of Object.values(sql.sub)) {
  160. if (!Array.isArray(value)) continue;
  161. // delete old related data first
  162. await trx(table).whereIn(key, updated).del();
  163. // for each updated item
  164. for (const identity of updated) {
  165. // insert value
  166. for (const current of value) {
  167. if (typeof current == 'object') {
  168. current[key] = identity;
  169. await trx(table).insert(current);
  170. } else {
  171. if (values.length != 1) throw new Error('Invalid value mapping');
  172. await trx(table).insert({
  173. [key]: identity,
  174. [values[0].column]: current
  175. });
  176. }
  177. }
  178. }
  179. }
  180. return { affected: updated.length };
  181. });
  182. }
  183. let affected = await db.fromJSON(sql);
  184. return { affected };
  185. },
  186. delete: async function(options) {
  187. const connection = this.parseRequired(options.connection, 'string', 'dbconnector.delete: connection is required.');
  188. const sql = this.parseSQL(options.sql);
  189. const db = this.getDbConnection(connection);
  190. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  191. if (!sql) throw new Error('dbconnector.delete: sql is required.');
  192. if (!sql.table) throw new Error('dbconnector.delete: sql.table is required.');
  193. sql.type = 'del';
  194. if (db.client == 'couchdb') {
  195. let { rows } = await db.list({ include_docs: true, startkey: sql.table + '/', endkey: sql.table + '0' });
  196. rows = rows.map(row => row.doc);
  197. if (sql.wheres) {
  198. const validate = (row, rule) => {
  199. if (rule.operator) {
  200. let a = row[rule.data.column];
  201. let b = rule.value;
  202. switch (rule.operator) {
  203. case 'equal': return a == b;
  204. case 'not_equal': return a != b;
  205. case 'in': return b.includes(a);
  206. case 'not_in': return !b.includes(a);
  207. case 'less': return a < b;
  208. case 'less_or_equal': return a <= b;
  209. case 'greater': return a > b;
  210. case 'greater_or_equal': return a >= b;
  211. case 'between': return b[0] <= a <= b[1];
  212. case 'not_between': return !(b[0] <= a <= b[1]);
  213. case 'begins_with': return String(a).startsWith(String(b));
  214. case 'not_begins_with': return !String(a).startsWith(String(b));
  215. case 'contains': return String(a).includes(String(b));
  216. case 'not_contains': return !String(a).includes(String(b));
  217. case 'ends_with': return String(a).endsWith(String(b));
  218. case 'not_ends_with': return !String(a).endsWith(String(b));
  219. case 'is_empty': return a == null || a == '';
  220. case 'is_not_empty': return a != null && a != '';
  221. case 'is_null': return a == null;
  222. case 'is_not_null': return a != null;
  223. }
  224. }
  225. if (rule.condition && rule.rules.length) {
  226. for (const _rule of rule.rules) {
  227. const valid = validate(row, _rule);
  228. if (!valid && rule.condition == 'AND') return false;
  229. if (valid && rule.condition == 'OR') return true;
  230. }
  231. return rule.condition == 'OR' ? false : true;
  232. }
  233. return true;
  234. };
  235. rows = rows.filter(row => {
  236. return validate(row, sql.wheres);
  237. });
  238. }
  239. const result = await db.bulk(rows.map(doc => {
  240. doc._deleted = true;
  241. return doc;
  242. }));
  243. return { affected: Array.isArray(result) ? result.filter(result => result.ok).length : rows.length };
  244. }
  245. if (options.test) {
  246. return {
  247. options: options,
  248. query: sql.toString()
  249. };
  250. }
  251. if (sql.sub) {
  252. return db.transaction(async trx => {
  253. const deleted = (await trx.fromJSON(sql)).map(value => value[sql.returning] || value);
  254. // loop sub
  255. for (let { table, key } of Object.values(sql.sub)) {
  256. // delete related data
  257. await trx(table).whereIn(key, deleted).del();
  258. }
  259. return { affected: deleted.length };
  260. });
  261. }
  262. let affected = await db.fromJSON(sql);
  263. return { affected };
  264. },
  265. custom: async function(options) {
  266. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.custom: connection is required.');
  267. const sql = this.parseSQL(options.sql);
  268. const db = this.getDbConnection(connection);
  269. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  270. if (!sql) throw new Error('dbconnector.custom: sql is required.');
  271. if (typeof sql.query != 'string') throw new Error('dbupdater.custom: sql.query is required.');
  272. if (!Array.isArray(sql.params)) throw new Error('dbupdater.custom: sql.params is required.');
  273. if (db.client == 'couchdb') {
  274. throw new Error('dbupdater.custom: couchdb is not supported.');
  275. }
  276. const params = [];
  277. const query = sql.query.replace(/([:@][a-zA-Z_]\w*|\?)/g, param => {
  278. if (param == '?') {
  279. params.push(sql.params[params.length].value);
  280. return '?';
  281. }
  282. let p = sql.params.find(p => p.name == param);
  283. if (p) {
  284. params.push(p.value);
  285. return '?';
  286. }
  287. return param;
  288. });
  289. let results = await db.raw(query, params);
  290. if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') {
  291. results = results[0];
  292. } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') {
  293. results = results.rows;
  294. }
  295. return results;
  296. },
  297. execute: async function(options) {
  298. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.execute: connection is required.');
  299. const query = this.parseRequired(options.query, 'string', 'dbupdater.execute: query is required.');
  300. const params = this.parseOptional(options.params, 'object', []);
  301. const db = this.getDbConnection(connection);
  302. if (!db) throw new Error(`Connection "${connection}" doesn't exist.`);
  303. if (db.client == 'couchdb') {
  304. throw new Error('dbupdater.execute: couchdb is not supported.');
  305. }
  306. let results = await db.raw(query, params);
  307. if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') {
  308. results = results[0];
  309. } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') {
  310. results = results.rows;
  311. }
  312. return results;
  313. },
  314. // bulk insert
  315. // values is array of objects with column(key) and value
  316. // batchSize is number of inserts per query (multi insert statement)
  317. bulkinsert: async function(options) {
  318. const connection = this.parseRequired(options.connection, 'string', 'dbupdater.bulkinsert: connection is required.');
  319. const table = this.parseRequired(options.table, 'string', 'dbupdater.bulkinsert: table is required.');
  320. const values = this.parseRequired(options.values, 'object', 'dbupdater.bulkinsert: values is required.');
  321. const batchSize = this.parseOptional(options.batchSize, 'number', 100);
  322. const db = this.getDbConnection(connection);
  323. if (db.client == 'couchdb') {
  324. throw new Error('dbupdater.bulkinsert: couchdb is not supported.');
  325. }
  326. await db.transaction(async transaction => {
  327. for (let i = 0; i < values.length; i += batchSize) {
  328. let batch = values.slice(i, i + batchSize);
  329. await transaction(table).insert(batch);
  330. }
  331. });
  332. },
  333. };