@axiosleo/orm-mysql 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env CHANGED
@@ -3,4 +3,4 @@ MYSQL_HOST = "rm-bp1a906h4019pldm6io.mysql.rds.aliyuncs.com"
3
3
  MYSQL_USER = "kms"
4
4
  MYSQL_PASS = "bmPWuq_vJgupdL9"
5
5
  MYSQL_PORT = 3306
6
- MYSQL_DB = "business_5b5ecd9941d6cc1bbc065fc6"
6
+ MYSQL_DB = "kms-test"
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @axiosleo/orm-mysql
2
2
 
3
+
4
+ [![NPM version](https://img.shields.io/npm/v/@axiosleo/orm-mysql.svg?style=flat-square)](https://npmjs.org/package/@axiosleo/orm-mysql)
5
+ [![npm download](https://img.shields.io/npm/dm/@axiosleo/orm-mysql.svg?style=flat-square)](https://npmjs.org/package/@axiosleo/orm-mysql)
6
+ [![License](https://img.shields.io/github/license/AxiosLeo/node-orm-mysql?color=%234bc524)](LICENSE)
7
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FAxiosLeo%2Fnode-orm-mysql.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FAxiosLeo%2Fnode-orm-mysql?ref=badge_shield)
8
+
3
9
  ## Installation
4
10
 
5
11
  ```bash
@@ -46,7 +52,7 @@ query.offset(0); // set offset
46
52
  let rows = await query.select(); // select
47
53
  ```
48
54
 
49
- ### Some Examples
55
+ ### Some Query Examples
50
56
 
51
57
  ```javascript
52
58
  const { createClient, QueryHandler, Query } = require("@axiosleo/orm-mysql");
@@ -129,6 +135,59 @@ async function subqueryExample() {
129
135
  }
130
136
  ```
131
137
 
138
+ ### Hook
139
+
140
+ ```javascript
141
+ const { Hook } = require("@axiosleo/orm-mysql");
142
+
143
+ // opt: 'select' | 'find' | 'insert' | 'update' | 'delete' | 'count'
144
+
145
+ Hook.pre(async (options) => {
146
+ debug.log('options', options);
147
+ }, { table: 'table_name', opt: 'insert'});
148
+
149
+ Hook.post(async (options, result) => {
150
+ throw new Error('some error');
151
+ }, { table: 'table_name', opt: 'insert' });
152
+ ```
153
+
154
+ ### Transaction
155
+
156
+ ```javascript
157
+ const { TransactionHandler, createPromiseClient } = require("@axiosleo/orm-mysql");
158
+
159
+ const conn = createPromiseClient({
160
+ host: process.env.MYSQL_HOST,
161
+ port: process.env.MYSQL_PORT,
162
+ user: process.env.MYSQL_USER,
163
+ password: process.env.MYSQL_PASS,
164
+ database: process.env.MYSQL_DB,
165
+ });
166
+
167
+ const transaction = new TransactionHandler(connection);
168
+
169
+ try {
170
+ // insert user info
171
+ let row = await transaction.table("users").insert({
172
+ name: "Joe",
173
+ age: 18,
174
+ });
175
+ const lastInsertId = row[0].insertId;
176
+
177
+ // insert student info
178
+ await transaction.table("students").insert({
179
+ user_id: lastInsertId,
180
+ });
181
+ await transaction.commit();
182
+ } catch (e) {
183
+ await transaction.rollback();
184
+ throw e;
185
+ }
186
+ ```
187
+
132
188
  ## License
133
189
 
134
190
  This project is open-sourced software licensed under the [MIT](LICENSE).
191
+
192
+
193
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FAxiosLeo%2Fnode-orm-mysql.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FAxiosLeo%2Fnode-orm-mysql?ref=badge_large)
package/index.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import {
2
+ Pool,
2
3
  OkPacket,
3
4
  Connection,
5
+ PoolOptions,
4
6
  QueryOptions,
5
7
  RowDataPacket,
6
8
  ConnectionOptions
7
9
  } from 'mysql2';
8
10
 
11
+ import {
12
+ Connection as PromiseConnection,
13
+ } from 'mysql2/promise';
14
+
9
15
  export type Clients = {
10
16
  [key: string]: Connection
11
17
  }
@@ -30,7 +36,7 @@ export interface OrderByOptions {
30
36
  export type OperatorType = 'select' | 'find' | 'insert' | 'update' | 'delete' | 'count';
31
37
 
32
38
  export interface JoinOption {
33
- table: string;
39
+ table: string | Query;
34
40
  table_alias?: string;
35
41
  self_column: string;
36
42
  foreign_column: string;
@@ -54,6 +60,8 @@ export interface QueryOperatorOptions {
54
60
  groupField: string[];
55
61
  joins: JoinOption[];
56
62
  having: WhereOptions[];
63
+ suffix?: string | null;
64
+ transaction: boolean;
57
65
  }
58
66
 
59
67
  export declare class Query {
@@ -90,6 +98,8 @@ export declare class Query {
90
98
  join(opt: JoinOption): this;
91
99
  }
92
100
 
101
+ export type QueryResult = any | undefined | RowDataPacket[] | RowDataPacket | OkPacket;
102
+
93
103
  export declare class QueryOperator extends Query {
94
104
  conn: Connection;
95
105
  options: QueryOperatorOptions
@@ -98,7 +108,7 @@ export declare class QueryOperator extends Query {
98
108
 
99
109
  buildSql(operator: OperatorType): { sql: string, values: any[] };
100
110
 
101
- exec(): Promise<any | undefined | RowDataPacket[] | RowDataPacket | OkPacket>;
111
+ exec(): Promise<QueryResult>;
102
112
 
103
113
  select<T>(): Promise<T[]>;
104
114
 
@@ -110,7 +120,12 @@ export declare class QueryOperator extends Query {
110
120
 
111
121
  count(): Promise<number>;
112
122
 
113
- delete(id?: number): Promise<OkPacket>;
123
+ /**
124
+ * delete data
125
+ * @param id
126
+ * @param index_field_name default is 'id'
127
+ */
128
+ delete(id?: number, index_field_name?: string): Promise<OkPacket>;
114
129
  }
115
130
 
116
131
  export declare class QueryHandler {
@@ -125,6 +140,51 @@ export declare class QueryHandler {
125
140
  upsert(tableName: string, data: any, condition: Record<string, ConditionValueType>): Promise<OkPacket>;
126
141
  }
127
142
 
143
+ export declare class TransactionOperator extends QueryOperator {
144
+ append(suffix: string): this;
145
+ }
146
+
147
+ export declare class TransactionHandler {
148
+ constructor(conn: PromiseConnection, options?: {
149
+ level: 'READ UNCOMMITTED' | 'RU'
150
+ | 'READ COMMITTED' | 'RC'
151
+ | 'REPEATABLE READ' | 'RR'
152
+ | 'SERIALIZABLE' | 'S'
153
+ });
154
+
155
+ query(options: QueryOptions): Promise<any>;
156
+
157
+ execute(sql: string, values: any[]): Promise<any>;
158
+
159
+ lastInsertId(alias?: string): Promise<number>;
160
+
161
+ table(table: string, alias?: string | null): TransactionOperator;
162
+
163
+ begin(): Promise<void>;
164
+
165
+ commit(): Promise<void>;
166
+
167
+ rollback(): Promise<void>;
168
+
169
+ upsert(tableName: string, data: any, condition: Record<string, ConditionValueType>): Promise<OkPacket>;
170
+ }
171
+
128
172
  export function createClient(options: ConnectionOptions, name?: string | null | undefined): Connection;
129
173
 
130
174
  export function getClient(name: string): Connection;
175
+
176
+ export function createPool(options: PoolOptions, name?: string | null | undefined): Pool;
177
+
178
+ export function createPromiseClient(options: ConnectionOptions, name?: string | null | undefined): PromiseConnection;
179
+
180
+ export declare class Hook {
181
+ static pre: (
182
+ callback: (options: QueryOperatorOptions) => void,
183
+ option: { table?: string, opt?: OperatorType }
184
+ ) => string;
185
+
186
+ static post: (
187
+ callback: (options: QueryOperatorOptions, result: QueryResult | Error) => void,
188
+ option: { table?: string, opt?: OperatorType }
189
+ ) => string;
190
+ }
package/index.js CHANGED
@@ -6,13 +6,32 @@ const {
6
6
  Query
7
7
  } = require('./src/operator');
8
8
 
9
- const { createClient, getClient } = require('./src/client');
9
+ const {
10
+ TransactionOperator,
11
+ TransactionHandler
12
+ } = require('./src/transaction');
13
+
14
+ const {
15
+ getClient,
16
+ createPool,
17
+ createClient,
18
+ createPromiseClient
19
+ } = require('./src/client');
20
+
21
+ const { Hook } = require('./src/hook');
10
22
 
11
23
  module.exports = {
24
+ Hook,
25
+
12
26
  Query,
13
27
  QueryHandler,
14
28
  QueryOperator,
15
29
 
30
+ TransactionOperator,
31
+ TransactionHandler,
32
+
16
33
  getClient,
17
- createClient
34
+ createPool,
35
+ createClient,
36
+ createPromiseClient
18
37
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiosleo/orm-mysql",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "MySQL ORM tool",
5
5
  "keywords": [
6
6
  "mysql",
@@ -0,0 +1,104 @@
1
+ /* eslint-disable no-console */
2
+ 'use strict';
3
+
4
+ const EventEmitter = require('events');
5
+ const { debug } = require('@axiosleo/cli-tool');
6
+ const events = {}; // event tree
7
+ const hook = new EventEmitter();
8
+
9
+ const pushEvent = ({ label, table, opt, callback }) => {
10
+ label = label || '*';
11
+ if (!events[label]) {
12
+ events[label] = {};
13
+ }
14
+ table = table || '*';
15
+ if (!events[label][table]) {
16
+ events[label][table] = {};
17
+ }
18
+ opt = opt || '*';
19
+ if (!events[label][table][opt]) {
20
+ events[label][table][opt] = 0;
21
+ }
22
+ events[label][table][opt]++;
23
+ hook.on(`${label}::${table}::${opt}`, callback);
24
+ return { label, table, opt, callback };
25
+ };
26
+
27
+ const eventRecur = (curr, trace, step, paths, args) => {
28
+ if (step === trace.length) {
29
+ hook.emit(paths.join('::'), ...args);
30
+ return;
31
+ }
32
+ const t = trace[step];
33
+ if (curr['*']) {
34
+ paths[step] = '*';
35
+ eventRecur(curr[t], trace, step + 1, paths, args);
36
+ }
37
+ if (curr[t]) {
38
+ paths[step] = t;
39
+ eventRecur(curr[t], trace, step + 1, paths, args);
40
+ }
41
+ return;
42
+ };
43
+
44
+ const handleEvent = (label, table, opt, ...args) => {
45
+ let curr = events;
46
+ let step = 0;
47
+ let trace = [label, table, opt];
48
+ eventRecur(curr, trace, step, [], args);
49
+ };
50
+
51
+ pushEvent({
52
+ label: 'before',
53
+ table: 'table1',
54
+ opt: 'insert',
55
+ callback: (...args) => {
56
+ debug.log(args);
57
+ }
58
+ });
59
+ pushEvent({
60
+ table: 'table1',
61
+ opt: 'insert',
62
+ callback: (...args) => {
63
+ debug.log(args);
64
+ }
65
+ });
66
+ pushEvent({
67
+ label: 'before',
68
+ table: 'table1',
69
+ callback: (...args) => {
70
+ debug.log(args);
71
+ }
72
+ });
73
+ pushEvent({
74
+ label: 'before',
75
+ opt: 'insert',
76
+ callback: (...args) => {
77
+ debug.log(args);
78
+ }
79
+ });
80
+ pushEvent({
81
+ label: 'before',
82
+ callback: (...args) => {
83
+ debug.log(args);
84
+ }
85
+ });
86
+ pushEvent({
87
+ table: 'table1',
88
+ callback: (...args) => {
89
+ debug.log(args);
90
+ }
91
+ });
92
+ pushEvent({
93
+ opt: 'insert',
94
+ callback: (...args) => {
95
+ debug.log(args);
96
+ }
97
+ });
98
+ pushEvent({
99
+ callback: (...args) => {
100
+ debug.log(args);
101
+ }
102
+ });
103
+ debug.log(JSON.stringify(events, null, 2));
104
+ handleEvent('before', 'table1', 'insert', 1, 2, 3);
package/runtimes/test.js CHANGED
@@ -1,45 +1,73 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable no-console */
1
3
  'use strict';
2
4
 
5
+ const path = require('path');
3
6
  const dotenv = require('dotenv');
4
- dotenv.config();
7
+ dotenv.config({
8
+ path: path.join(__dirname, '../.env')
9
+ });
5
10
  const { debug } = require('@axiosleo/cli-tool');
6
11
 
7
- const mysql = require('mysql2');
8
- const {
9
- QueryHandler,
10
- // Query
11
- } = require('../src/operator');
12
-
13
- const conn = mysql.createConnection({
14
- host: process.env.MYSQL_HOST,
15
- port: process.env.MYSQL_PORT,
16
- user: process.env.MYSQL_USER,
17
- password: process.env.MYSQL_PASS,
18
- database: process.env.MYSQL_DB,
19
- });
20
- const hanlder = new QueryHandler(conn);
12
+ const mysql = require('mysql2/promise');
21
13
 
22
- const test = async () => {
23
- const query = hanlder.table('meta_items_relationship', 'mir')
24
- .attr('mi_p.meta as p_meta', 'mi_c.meta as c_meta', 'mir.item_parent as item_parent_id', 'mir.item_child as item_child_id');
25
- const res = query
26
- .where('mi_p.id', 601)
27
- .join({
28
- table: 'meta_items',
29
- table_alias: 'mi_p',
30
- self_column: 'mir.item_parent',
31
- foreign_column: 'mi_p.id',
32
- // join_type: 'left'
33
- })
34
- .join({
35
- table: 'meta_items',
36
- table_alias: 'mi_c',
37
- self_column: 'mir.item_child',
38
- foreign_column: 'mi_c.id',
39
- }).buildSql('select');
40
- const result = await query.select();
41
- debug.halt(res, result);
42
- };
14
+ const { TransactionHandler } = require('../src/transaction');
43
15
 
44
- test();
16
+ async function main() {
17
+ const items = ['RI0002', 'CB0004'];
18
+ const config = {
19
+ user: process.env.MYSQL_USER,
20
+ password: process.env.MYSQL_PASS,
21
+ host: process.env.MYSQL_HOST,
22
+ port: process.env.MYSQL_PORT,
23
+ database: process.env.MYSQL_DB,
24
+ };
25
+ const connection = await mysql.createConnection(config);
26
+ const transaction = new TransactionHandler(connection);
27
+ await transaction.begin();
28
+ console.log('Finished setting the isolation level to read committed');
29
+ try {
30
+ await transaction.table('product').attr('id', 'name').where('sku', items, 'IN').append('FOR UPDATE').select();
31
+ console.log(`Locked rows for skus ${items.join()}`);
32
+ const [itemsToOrder] = await transaction.table('product').attr('name', 'quantity', 'price').where('sku', items, 'IN').orderBy('id').select();
33
+ console.log('Selected quantities for items');
34
+ let orderTotal = 0;
35
+ let orderItems = [];
36
+ for (let itemToOrder of itemsToOrder) {
37
+ if (itemToOrder.quantity < 1) {
38
+ throw new Error(`One of the items is out of stock ${itemToOrder.name}`);
39
+ }
40
+ console.log(`Quantity for ${itemToOrder.name} is ${itemToOrder.quantity}`);
41
+ orderTotal += itemToOrder.price;
42
+ orderItems.push(itemToOrder.name);
43
+ }
44
+ const res = await transaction.table('sales_order').insert({
45
+ items: orderItems.join(),
46
+ total: orderTotal,
47
+ });
48
+ // const lastInsertId = res[0].insertId;
49
+ debug.log('result', res);
50
+ await debug.pause('pause', {
51
+ items: orderItems.join(),
52
+ total: orderTotal,
53
+ });
54
+ console.log('Order created');
55
+ await transaction.execute(
56
+ 'UPDATE product SET quantity=quantity - 1 WHERE sku IN (?, ?)',
57
+ items
58
+ );
59
+ console.log(`Deducted quantities by 1 for ${items.join()}`);
60
+ await transaction.commit();
61
+ const lastInsertId = await transaction.lastInsertId('order_id');
62
+ debug.log(`order created with id ${lastInsertId}`);
63
+ return `order created with id ${lastInsertId}`;
64
+ } catch (err) {
65
+ console.error(`Error occurred while creating order: ${err.message}`, err);
66
+ transaction.rollback();
67
+ console.info('Rollback successful');
68
+ debug.log('error creating order');
69
+ return 'error creating order';
70
+ }
71
+ }
45
72
 
73
+ main();
package/src/builder.js CHANGED
@@ -3,10 +3,21 @@
3
3
  const Query = require('./query');
4
4
  const is = require('@axiosleo/cli-tool/src/helper/is');
5
5
 
6
+ /**
7
+ * @param {array} arr
8
+ * @param {string} res
9
+ */
10
+ const emit = (arr, res) => {
11
+ if (res) {
12
+ arr.push(res);
13
+ }
14
+ };
15
+
6
16
  class Builder {
7
17
  constructor(options) {
8
18
  let sql = '';
9
19
  this.values = [];
20
+ let tmp = [];
10
21
  switch (options.operator) {
11
22
  case 'find': {
12
23
  options.pageLimit = 1;
@@ -14,51 +25,59 @@ class Builder {
14
25
  }
15
26
  // eslint-disable-next-line no-fallthrough
16
27
  case 'select': {
17
- sql = `SELECT ${options.attrs ? options.attrs.map((a) => this._buildFieldKey(a)).join(',') : '*'} FROM ${this._buildTables(options.tables)}`;
18
- sql += this._buildJoins(options.joins);
19
- sql += this._buildContidion(options.conditions);
20
- sql += options.orders.length > 0 ? this._buildOrders(options.orders) : '';
21
- sql += this._buldPagenation(options.pageLimit, options.pageOffset);
22
- if (options.groupField.length) {
23
- sql += ` GROUP BY ${options.groupField.map(f => this._buildFieldKey(f)).join(',')}`;
24
- sql += this._buildHaving(options.having);
25
- } else if (options.having && options.having.length) {
28
+ emit(tmp, `SELECT ${options.attrs ? options.attrs.map((a) => this._buildFieldKey(a)).join(',') : '*'} FROM ${this._buildTables(options.tables)}`);
29
+ emit(tmp, this._buildJoins(options.joins));
30
+ emit(tmp, this._buildContidion(options.conditions));
31
+ emit(tmp, this._buildOrders(options.orders));
32
+ emit(tmp, this._buldPagenation(options.pageLimit, options.pageOffset));
33
+ if (options.having && options.having.length && !options.groupField.length) {
26
34
  throw new Error('having is not allowed without "GROUP BY"');
27
35
  }
36
+ emit(tmp, this._buildGroupField(options.groupField));
37
+ emit(tmp, this._buildHaving(options.having));
38
+ sql = tmp.join(' ');
39
+ if (options.suffix) {
40
+ sql += ' ' + options.suffix;
41
+ }
28
42
  break;
29
43
  }
30
44
  case 'insert': {
31
45
  const fields = this._buildValues(options.data);
32
- sql = `INSERT INTO ${this._buildTables(options.tables)}(${fields.map((f) => this._buildFieldKey(f))}) VALUES (${fields.map(() => '?').join(',')})`;
46
+ emit(tmp, `INSERT INTO ${this._buildTables(options.tables)}(${fields.map((f) => `\`${f}\``).join(',')})`);
47
+ emit(tmp, `VALUES (${fields.map((f) => '?').join(',')})`);
48
+ sql = tmp.join(' ');
33
49
  break;
34
50
  }
35
51
  case 'update': {
36
52
  const fields = this._buildValues(options.data);
37
- sql = `UPDATE ${this._buildTables(options.tables)} SET ${fields.map(f => `${this._buildFieldKey(f)} = ?`).join(',')}`;
53
+ emit(tmp, `UPDATE ${this._buildTables(options.tables)}`);
54
+ emit(tmp, `SET ${fields.map((f) => `\`${f}\` = ?`).join(',')}`);
38
55
  if (!options.conditions.length) {
39
56
  throw new Error('At least one condition is required for update operation');
40
57
  }
41
- sql += this._buildContidion(options.conditions);
58
+ emit(tmp, this._buildContidion(options.conditions));
59
+ sql = tmp.join(' ');
42
60
  break;
43
61
  }
44
62
  case 'delete': {
45
- sql = `DELETE FROM ${this._buildTables(options.tables)}`;
63
+ emit(tmp, `DELETE FROM ${this._buildTables(options.tables)}`);
46
64
  if (!options.conditions.length) {
47
65
  throw new Error('At least one where condition is required for delete operation');
48
66
  }
49
- sql += this._buildContidion(options.conditions);
67
+ emit(tmp, this._buildContidion(options.conditions));
68
+ sql = tmp.join(' ');
50
69
  break;
51
70
  }
52
71
  case 'count': {
53
- sql = `SELECT COUNT(*) AS count FROM ${this._buildTables(options.tables)}`;
54
- sql += this._buildJoins(options.joins);
55
- sql += this._buildContidion(options.conditions);
56
- if (options.groupField.length) {
57
- sql += ` GROUP BY ${options.groupField.map(f => this._buildFieldKey(f)).join(',')}`;
58
- sql += this._buildHaving(options.having);
59
- } else if (options.having && options.having.length) {
72
+ emit(tmp, `SELECT COUNT(*) AS count FROM ${this._buildTables(options.tables)}`);
73
+ emit(tmp, this._buildJoins(options.joins));
74
+ emit(tmp, this._buildContidion(options.conditions));
75
+ if (options.having && options.having.length && !options.groupField.length) {
60
76
  throw new Error('having is not allowed without "GROUP BY"');
61
77
  }
78
+ emit(tmp, this._buildGroupField(options.groupField));
79
+ emit(tmp, this._buildHaving(options.having));
80
+ sql = tmp.join(' ');
62
81
  break;
63
82
  }
64
83
  default:
@@ -68,16 +87,31 @@ class Builder {
68
87
  this.sql = sql;
69
88
  }
70
89
 
90
+ _buildGroupField(groupFields = []) {
91
+ if (!groupFields || !groupFields.length) {
92
+ return '';
93
+ }
94
+ return `GROUP BY ${groupFields.map(f => this._buildFieldKey(f)).join(',')}`;
95
+ }
96
+
71
97
  _buildHaving(having) {
72
- if (!having.length) {
98
+ if (!having || !having.length) {
73
99
  return '';
74
100
  }
75
- return this._buildContidion(having, ' HAVING ');
101
+ return this._buildContidion(having, 'HAVING ');
76
102
  }
77
103
 
78
104
  _buildJoins(joins = []) {
79
105
  return joins.map((j) => {
80
106
  let { table, alias, self_column, foreign_column, join_type } = j;
107
+ if (table instanceof Query) {
108
+ if (!alias) {
109
+ throw new Error('Alias is required for subquery');
110
+ }
111
+ const builder = new Builder(table.options);
112
+ this.values = this.values.concat(builder.values);
113
+ table = `(${builder.sql})`;
114
+ }
81
115
  if (alias) {
82
116
  table = `\`${table}\` AS \`${alias}\``;
83
117
  } else {
@@ -86,13 +120,13 @@ class Builder {
86
120
  let sql = '';
87
121
  switch (join_type) {
88
122
  case 'left':
89
- sql = ' LEFT JOIN ';
123
+ sql = 'LEFT JOIN ';
90
124
  break;
91
125
  case 'right':
92
- sql = ' RIGHT JOIN ';
126
+ sql = 'RIGHT JOIN ';
93
127
  break;
94
128
  default:
95
- sql = ' INNER JOIN ';
129
+ sql = 'INNER JOIN ';
96
130
  break;
97
131
  }
98
132
  sql += `${table} ON ${this._buildFieldWithTableName(self_column)} = ${this._buildFieldWithTableName(foreign_column)}`;
@@ -101,14 +135,17 @@ class Builder {
101
135
  }
102
136
 
103
137
  _buildOrders(orders = []) {
104
- const sql = ' ORDER BY ' + orders.map((o) => {
138
+ if (!orders || !orders.length) {
139
+ return '';
140
+ }
141
+ const sql = 'ORDER BY ' + orders.map((o) => {
105
142
  return `${this._buildFieldKey(o.sortField)} ${o.sortOrder}`;
106
143
  }).join(',');
107
144
  return sql;
108
145
  }
109
146
 
110
147
  _buildTables(tables) {
111
- if (!tables.length) {
148
+ if (!tables || !tables.length) {
112
149
  throw new Error('At least one table is required');
113
150
  }
114
151
  return tables.map((t) => {
@@ -156,10 +193,10 @@ class Builder {
156
193
  }
157
194
 
158
195
  _buildContidion(conditions, prefix) {
159
- if (!conditions.length) {
196
+ if (!conditions || !conditions.length) {
160
197
  return '';
161
198
  }
162
- let sql = typeof prefix === 'undefined' ? ' WHERE ' : prefix;
199
+ let sql = typeof prefix === 'undefined' ? 'WHERE ' : prefix;
163
200
  if (conditions.length) {
164
201
  sql += `${conditions.map((c) => {
165
202
  if (c.key === null && c.value === null) {
package/src/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const mysql = require('mysql2');
4
+ const mysqlPromise = require('mysql2/promise');
4
5
  const { validate } = require('./utils');
5
6
 
6
7
  const clients = {};
@@ -28,6 +29,51 @@ const createClient = (options, name = null) => {
28
29
  return clients[key];
29
30
  };
30
31
 
32
+ /**
33
+ * @param {mysql.ConnectionOptions} options
34
+ * @param {string|null} name
35
+ * @returns {mysqlPromise.Connection}
36
+ */
37
+ const createPromiseClient = async (options, name = null) => {
38
+ validate(options, {
39
+ host: 'required|string',
40
+ user: 'required|string',
41
+ password: 'required|string',
42
+ port: 'required|integer',
43
+ database: 'required|string',
44
+ });
45
+ const key = name ? name :
46
+ `${options.host}:${options.port}:${options.user}:${options.password}:${options.database}`;
47
+ if (clients[key]) {
48
+ return clients[key];
49
+ }
50
+ clients[key] = await mysqlPromise.createConnection(options);
51
+ return clients[key];
52
+ };
53
+
54
+ /**
55
+ * create pool
56
+ * @param {mysql.PoolOptions} options
57
+ * @returns {mysql.Pool}
58
+ */
59
+ const createPool = (options, name = null) => {
60
+ validate(options, {
61
+ host: 'required|string',
62
+ user: 'required|string',
63
+ password: 'required|string',
64
+ port: 'required|integer',
65
+ database: 'required|string',
66
+ });
67
+ const key = name ? name :
68
+ `${options.host}:${options.port}:${options.user}:${options.password}:${options.database}`;
69
+ if (clients[key]) {
70
+ return clients[key];
71
+ }
72
+ const pool = mysql.createPool(options);
73
+ clients[key] = pool;
74
+ return pool;
75
+ };
76
+
31
77
  /**
32
78
  * get client
33
79
  * @param {*} name
@@ -45,5 +91,7 @@ const getClient = (name) => {
45
91
 
46
92
  module.exports = {
47
93
  getClient,
48
- createClient
94
+ createPool,
95
+ createClient,
96
+ createPromiseClient
49
97
  };
package/src/hook.js ADDED
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+
5
+ const events = {}; // event tree
6
+ const hook = new EventEmitter();
7
+
8
+ const pushEvent = ({ label, table, opt, callback }) => {
9
+ label = label || '*';
10
+ if (!events[label]) {
11
+ events[label] = {};
12
+ }
13
+ table = table || '*';
14
+ if (!events[label][table]) {
15
+ events[label][table] = {};
16
+ }
17
+ opt = opt || '*';
18
+ if (!events[label][table][opt]) {
19
+ events[label][table][opt] = 0;
20
+ }
21
+ events[label][table][opt]++;
22
+ hook.on(`${label}::${table}::${opt}`, callback);
23
+ return { label, table, opt, callback };
24
+ };
25
+
26
+ const eventRecur = (curr, trace, step, paths, args) => {
27
+ if (step === trace.length) {
28
+ hook.emit(paths.join('::'), ...args);
29
+ return;
30
+ }
31
+ const t = trace[step];
32
+ if (curr['*']) {
33
+ paths[step] = '*';
34
+ eventRecur(curr[t], trace, step + 1, paths, args);
35
+ }
36
+ if (curr[t]) {
37
+ paths[step] = t;
38
+ eventRecur(curr[t], trace, step + 1, paths, args);
39
+ }
40
+ return;
41
+ };
42
+
43
+ const handleEvent = (label, table, opt, ...args) => {
44
+ let curr = events;
45
+ let step = 0;
46
+ let trace = [label, table, opt];
47
+ eventRecur(curr, trace, step, [], args);
48
+ };
49
+
50
+ class Hook {
51
+ static pre(callback, { table, opt }) {
52
+ return pushEvent({
53
+ label: 'pre', table, opt, callback
54
+ });
55
+ }
56
+
57
+ static post(callback, { table, opt }) {
58
+ return pushEvent({
59
+ label: 'post', table, opt, callback
60
+ });
61
+ }
62
+ }
63
+
64
+ module.exports = {
65
+ Hook,
66
+ handleEvent
67
+ };
package/src/operator.js CHANGED
@@ -2,16 +2,23 @@
2
2
 
3
3
  const { Builder } = require('./builder');
4
4
  const Query = require('./query');
5
+ const { handleEvent } = require('./hook');
5
6
 
6
- const query = async (conn, options) => {
7
+ const query = async (conn, options, transaction) => {
7
8
  return new Promise((resolve, reject) => {
8
- conn.query(options, (err, result) => {
9
- if (err) {
10
- reject(err);
11
- } else {
12
- resolve(result);
13
- }
14
- });
9
+ if (transaction) {
10
+ conn.execute(options)
11
+ .then((res) => resolve(res))
12
+ .catch((err) => reject(err));
13
+ } else {
14
+ conn.query(options, (err, result) => {
15
+ if (err) {
16
+ reject(err);
17
+ } else {
18
+ resolve(result);
19
+ }
20
+ });
21
+ }
15
22
  });
16
23
  };
17
24
 
@@ -38,18 +45,30 @@ class QueryOperator extends Query {
38
45
  const options = {
39
46
  sql, values
40
47
  };
41
- switch (this.options.operator) {
42
- case 'find': {
43
- const res = await query(this.conn, options);
44
- return res[0];
45
- }
46
- case 'count': {
47
- const [res] = await query(this.conn, options);
48
- return res.count;
48
+ const from = this.options.tables.map(t => t.tableName).join(',');
49
+ handleEvent('pre', from, this.options.operator, this.options);
50
+ let res;
51
+ try {
52
+ switch (this.options.operator) {
53
+ case 'find': {
54
+ const tmp = await query(this.conn, options, this.options.transaction);
55
+ res = tmp[0];
56
+ break;
57
+ }
58
+ case 'count': {
59
+ const [tmp] = await query(this.conn, options, this.options.transaction);
60
+ res = tmp.count;
61
+ break;
62
+ }
63
+ default:
64
+ res = await query(this.conn, options, this.options.transaction);
49
65
  }
50
- default:
51
- return query(this.conn, options);
66
+ handleEvent('post', from, this.options.operator, this.options, res);
67
+ } catch (err) {
68
+ handleEvent('post', from, this.options.operator, this.options, err);
69
+ throw err;
52
70
  }
71
+ return res;
53
72
  }
54
73
 
55
74
  async select() {
@@ -83,9 +102,9 @@ class QueryOperator extends Query {
83
102
  return await this.exec();
84
103
  }
85
104
 
86
- async delete(id) {
105
+ async delete(id, index_field_name = 'id') {
87
106
  if (id) {
88
- this.where('id', id);
107
+ this.where(index_field_name, id);
89
108
  }
90
109
  this.options.operator = 'delete';
91
110
  return await this.exec();
package/src/query.js CHANGED
@@ -10,7 +10,9 @@ class Query {
10
10
  data: null,
11
11
  groupField: [],
12
12
  having: [],
13
- joins: []
13
+ joins: [],
14
+ suffix: null,
15
+ transaction: false
14
16
  };
15
17
  }
16
18
 
@@ -119,6 +121,7 @@ class Query {
119
121
  let lastOpt = this.options.having[this.options.having.length - 1].opt.toUpperCase();
120
122
  if (lastOpt !== 'AND' && lastOpt !== 'OR') {
121
123
  this.options.having.push({ key: null, opt: 'AND', value: null });
124
+ return this;
122
125
  }
123
126
  }
124
127
  this.options.having.push({ key, opt, value });
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ // eslint-disable-next-line no-unused-vars
4
+ const mysql = require('mysql2/promise');
5
+ const { QueryOperator } = require('./operator');
6
+
7
+ const levels = {
8
+ RU: 'READ UNCOMMITTED',
9
+ RC: 'READ COMMITTED',
10
+ RR: 'REPEATABLE READ',
11
+ S: 'SERIALIZABLE'
12
+ };
13
+
14
+ class TransactionOperator extends QueryOperator {
15
+ /**
16
+ * @param {*} conn
17
+ */
18
+ constructor(conn) {
19
+ super(conn);
20
+ this.options.transaction = true;
21
+ }
22
+
23
+ /**
24
+ * @example LOCK IN SHARE MODE
25
+ * @example FOR UPDATE
26
+ */
27
+ append(suffix) {
28
+ this.options.suffix = suffix || null;
29
+ return this;
30
+ }
31
+ }
32
+
33
+ class TransactionHandler {
34
+ /**
35
+ * @param {mysql.Connection} conn
36
+ * @param {mysql.ConnectionOptions} options
37
+ */
38
+ constructor(conn, options = {}) {
39
+ this.isbegin = false;
40
+ this.conn = conn;
41
+ this.level = options.level || 'SERIALIZABLE';
42
+ if (levels[this.level]) {
43
+ this.level = levels[this.level];
44
+ }
45
+ if (!Object.values(levels).includes(this.level)) {
46
+ throw new Error('Invalid transaction level: ' + this.level);
47
+ }
48
+ }
49
+
50
+ async query(options) {
51
+ return new Promise((resolve, reject) => {
52
+ this.conn.query(options, (err, result) => {
53
+ if (err) {
54
+ reject(err);
55
+ } else {
56
+ resolve(result);
57
+ }
58
+ });
59
+ });
60
+ }
61
+
62
+ async execute(sql, values = []) {
63
+ return this.conn.execute(sql, values);
64
+ }
65
+
66
+ async lastInsertId(alias = 'insert_id') {
67
+ let sql = `SELECT LAST_INSERT_ID() as ${alias}`;
68
+ const [row] = await this.execute(sql);
69
+ return row && row[0] ? row[0][alias] : 0;
70
+ }
71
+
72
+ async begin() {
73
+ this.isbegin = true;
74
+ await this.execute(`SET TRANSACTION ISOLATION LEVEL ${this.level}`);
75
+ await this.conn.beginTransaction();
76
+ }
77
+
78
+ table(table, alias = null) {
79
+ if (!this.isbegin) {
80
+ throw new Error('Transaction is not begin');
81
+ }
82
+ return (new TransactionOperator(this.conn)).table(table, alias);
83
+ }
84
+
85
+ async upsert(tableName, data, condition = {}) {
86
+ const count = await this.table(tableName).whereObject(condition).count();
87
+ if (count) {
88
+ return await this.table(tableName).whereObject(condition).update(data);
89
+ }
90
+ return await this.table(tableName).insert(data);
91
+ }
92
+
93
+ async commit() {
94
+ if (!this.isbegin) {
95
+ throw new Error('Transaction is not begin');
96
+ }
97
+ await this.conn.commit();
98
+ }
99
+
100
+ async rollback() {
101
+ if (!this.isbegin) {
102
+ throw new Error('Transaction is not begin');
103
+ }
104
+ await this.conn.rollback();
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ TransactionOperator,
110
+ TransactionHandler
111
+ };