@igojs/db 6.0.0-beta.1

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/src/Sql.js ADDED
@@ -0,0 +1,311 @@
1
+
2
+ const _ = require('lodash');
3
+
4
+ module.exports = class Sql {
5
+
6
+ constructor(query, dialect) {
7
+ this.query = query;
8
+ this.dialect = dialect;
9
+ this.i = 1;
10
+ }
11
+
12
+ // SELECT SQL
13
+ selectSQL() {
14
+ const { query, dialect } = this;
15
+ const { esc } = dialect;
16
+
17
+ let sql = 'SELECT ';
18
+ const params = [];
19
+
20
+ if (query.distinct) {
21
+ const joined = query.distinct.join(`${esc},${esc}`);
22
+ sql += `DISTINCT ${esc}${joined}${esc} `;
23
+ } else if (query.select) {
24
+ let select_sql = query.select;
25
+ _.each(query.joins, join => {
26
+ const [assoc_type, name, Obj] = join.association;
27
+ select_sql = select_sql.replace(new RegExp(`\\b${Obj.schema.table}\\b`, 'g'), name);
28
+ });
29
+ sql += select_sql + ' ';
30
+ } else {
31
+ sql += `${esc}${query.table}${esc}.*`;
32
+
33
+ _.each(query.joins, join => {
34
+ const { src_schema, association } = join;
35
+ const [ assoc_type, name, Obj, src_column, column] = association;
36
+ const table_alias = name;
37
+ sql += ', ';
38
+ if (join.columns) {
39
+ // add only specified columns
40
+ sql += _.map(join.columns, (column) => {
41
+ if (column.indexOf('.') > -1 || column.toLowerCase().indexOf(' as ') > -1) {
42
+ return column; // already qualified or aliased
43
+ }
44
+ //
45
+ return `${esc}${table_alias}${esc}.${esc}${column}${esc} as ${esc}${table_alias}__${column}${esc}`;
46
+ }).join(', ');
47
+ } else {
48
+ // add all columns from joined table
49
+ sql += _.map(Obj.schema.columns, (column) => {
50
+ return `${esc}${table_alias}${esc}.${esc}${column.name}${esc} as ${esc}${table_alias}__${column.name}${esc}`;
51
+ }).join(', ');
52
+ }
53
+ });
54
+ sql += ' ';
55
+ }
56
+
57
+ // from
58
+ sql += `FROM ${esc}${query.table}${esc} `;
59
+
60
+ // joins
61
+ sql += this.addJoins();
62
+
63
+ // where
64
+ sql += this.whereSQL(params);
65
+
66
+ // whereNot
67
+ sql += this.whereNotSQL(params);
68
+
69
+ // group
70
+ sql += this.groupSQL();
71
+
72
+ // order by
73
+ sql += this.orderSQL();
74
+
75
+
76
+ // limit
77
+ if (query.limit) {
78
+ sql += dialect.limit(this.i++, this.i++);
79
+ params.push(query.offset || 0);
80
+ params.push(query.limit);
81
+ }
82
+
83
+ const ret = {
84
+ sql: sql.trim(),
85
+ params: params
86
+ };
87
+ // console.dir(ret);
88
+ return ret;
89
+ };
90
+
91
+ // COUNT SQL
92
+ countSQL() {
93
+ const { query, dialect } = this;
94
+ const { esc } = dialect;
95
+
96
+ // select
97
+ let sql = `SELECT COUNT(0) as ${esc}count${esc} `;
98
+ const params = [];
99
+
100
+ // from
101
+ sql += `FROM ${esc}${query.table}${esc} `;
102
+
103
+ // joins
104
+ sql += this.addJoins();
105
+
106
+ // where
107
+ sql += this.whereSQL(params);
108
+
109
+ // whereNot
110
+ sql += this.whereNotSQL(params);
111
+
112
+ var ret = {
113
+ sql: sql.trim(),
114
+ params: params
115
+ };
116
+ // console.dir(ret);
117
+ return ret;
118
+ };
119
+
120
+ // JOINS
121
+ addJoins() {
122
+ const { query, dialect } = this;
123
+ const { esc } = dialect;
124
+
125
+ let sql = '';
126
+ _.each(query.joins, join => {
127
+ const { src_schema, type, association, src_alias } = join;
128
+ const [ assoc_type, name, Obj, src_column, column] = association;
129
+ const src_table_alias = src_alias || src_schema.table;
130
+ const table = Obj.schema.table;
131
+ sql += `${type.toUpperCase()} JOIN ${esc}${table}${esc} AS ${esc}${name}${esc} ON ${esc}${name}${esc}.${esc}${column}${esc} = ${esc}${src_table_alias}${esc}.${esc}${src_column}${esc} `;
132
+ });
133
+ return sql;
134
+ }
135
+
136
+ // WHERE
137
+ whereSQL(params, not) {
138
+ const { query, dialect } = this;
139
+ const { esc } = dialect;
140
+
141
+ const sqlwhere = [];
142
+ const wheres = not ? query.whereNot : query.where;
143
+ _.forEach(wheres, (where) => {
144
+ if (_.isArray(where)) {
145
+ // where is an array with sql string and params
146
+ if (not) {
147
+ console.warn('Where clause contains a string with whereNot, this is not supported');
148
+ return;
149
+ }
150
+ // where is an array
151
+ let s = where[0];
152
+ while (s.indexOf('$?') > -1) {
153
+ s = s.replace('$?', dialect.param(this.i++));
154
+ }
155
+ sqlwhere.push(s + ' ');
156
+ if (_.isArray(where[1])) {
157
+ Array.prototype.push.apply(params, where[1]);
158
+ } else {
159
+ params.push(where[1]);
160
+ }
161
+ } else if (_.isString(where)) {
162
+ // where is a string
163
+ if (not) {
164
+ console.warn('Where clause is a string with whereNot, this is not supported');
165
+ return;
166
+ }
167
+ sqlwhere.push(where + ' ');
168
+ } else {
169
+ // where is an object
170
+ _.forOwn(where, (value, key) =>{
171
+ let column_alias;
172
+ if (key.indexOf('.') > -1 && key.indexOf('`') === -1) {
173
+ column_alias = _.map(key.split('.'), (part) => (`${esc}${part}${esc}`)).join('.');
174
+ } else {
175
+ column_alias = `${esc}${query.table}${esc}.${esc}${key}${esc}`
176
+ }
177
+ if (value === null || value === undefined) {
178
+ sqlwhere.push(`${column_alias} IS ${not ? 'NOT NULL' : 'NULL'} `);
179
+ } else if (_.isArray(value) && value.length === 0) {
180
+ // where in empty array
181
+ sqlwhere.push(not ? 'TRUE ': 'FALSE ');
182
+ } else if (_.isArray(value)) {
183
+ sqlwhere.push(`${column_alias} ${not ? dialect.notin : dialect.in} (${dialect.param(this.i++)}) `);
184
+ params.push(value);
185
+ } else {
186
+ sqlwhere.push(`${column_alias} ${not ? '!=' : '='} ${dialect.param(this.i++)} `);
187
+ params.push(value);
188
+ }
189
+ });
190
+ }
191
+ });
192
+
193
+ if (sqlwhere.length) {
194
+ const ret = (not && query.where.length > 0) ? 'AND ' : 'WHERE ';
195
+ return ret + sqlwhere.join('AND ');
196
+ }
197
+ return '';
198
+ };
199
+
200
+ //
201
+ whereNotSQL(params) {
202
+ return this.whereSQL(params, true);
203
+ };
204
+
205
+ // INSERT SQL
206
+ insertSQL() {
207
+ const { query, dialect } = this;
208
+ const { esc } = dialect;
209
+
210
+ // insert into
211
+ let sql = `INSERT INTO ${esc}${query.table}${esc}`;
212
+
213
+ // columns
214
+ const columns = [], values = [], params = [];
215
+ _.forOwn(query.values, function(value, key) {
216
+ columns.push(`${esc}${key}${esc}`);
217
+ values.push(dialect.param(this.i++));
218
+ params.push(value);
219
+ }.bind(this));
220
+
221
+ if (!columns.length && dialect.emptyInsert) {
222
+ sql += dialect.emptyInsert;
223
+ } else {
224
+ sql += '(' + columns.join(',') + ') ';
225
+ sql += 'VALUES(' + values.join(',') + ') ';
226
+ }
227
+
228
+ sql += dialect.returning;
229
+ return { sql, params };
230
+ };
231
+
232
+ // UPDATE SQL
233
+ updateSQL() {
234
+ const { query, dialect } = this;
235
+ const { esc } = dialect;
236
+
237
+ // update set
238
+ let sql = `UPDATE ${esc}${query.table}${esc} SET `;
239
+
240
+ // columns
241
+ const columns = [], params = [];
242
+ _.forOwn(query.values, (value, key) => {
243
+ columns.push(`${esc}${key}${esc} = ${dialect.param(this.i++)}`);
244
+ params.push(value);
245
+ });
246
+
247
+ sql += columns.join(', ') + ' ';
248
+
249
+ // where
250
+ sql += this.whereSQL(params);
251
+
252
+ // whereNot
253
+ sql += this.whereNotSQL(params);
254
+
255
+ const ret = {
256
+ sql: sql,
257
+ params: params
258
+ };
259
+ return ret;
260
+ };
261
+
262
+ // DELETE SQL
263
+ deleteSQL() {
264
+ const { query, dialect } = this;
265
+ const { esc } = dialect;
266
+
267
+ // delete
268
+ let sql = `DELETE FROM ${esc}${query.table}${esc} `;
269
+
270
+ // columns
271
+ var params = [];
272
+
273
+ // where
274
+ sql += this.whereSQL(params);
275
+
276
+ // whereNot
277
+ sql += this.whereNotSQL(params);
278
+
279
+ var ret = {
280
+ sql: sql,
281
+ params: params
282
+ };
283
+ return ret;
284
+ };
285
+
286
+ // ORDER BY SQL
287
+ orderSQL() {
288
+ const { query } = this;
289
+
290
+ if (!query.order || !query.order.length) {
291
+ return '';
292
+ }
293
+
294
+ var sql = 'ORDER BY ' + query.order.join(', ') + ' ';
295
+
296
+ return sql;
297
+ };
298
+
299
+
300
+ // GROUP BY SQL
301
+ groupSQL() {
302
+ const { query } = this;
303
+
304
+ if (_.isEmpty(query.group )) {
305
+ return '';
306
+ }
307
+
308
+ var sql = 'GROUP BY ' + query.group.join(', ') + ' ';
309
+ return sql;
310
+ };
311
+ };
package/src/context.js ADDED
@@ -0,0 +1,12 @@
1
+ // Dependency injection container for @igojs/db
2
+ // These dependencies are injected by @igojs/server at initialization
3
+
4
+ const context = {
5
+ config: null,
6
+ cache: null,
7
+ logger: null,
8
+ utils: null,
9
+ errorhandler: null,
10
+ };
11
+
12
+ module.exports = context;
package/src/dbs.js ADDED
@@ -0,0 +1,26 @@
1
+
2
+
3
+ const _ = require('lodash');
4
+
5
+ const Db = require('./Db');
6
+ const context = require('./context');
7
+ const migrations = require('./migrations');
8
+
9
+ // init databases connections
10
+ module.exports.init = async () => {
11
+ const { config } = context;
12
+ for (const database of config.databases) {
13
+ const db = new Db(database);
14
+ await db.init();
15
+
16
+ if (config.databases.length === 1) {
17
+ db.config.migrations_dir = 'sql';
18
+ }
19
+
20
+ module.exports[database] = db;
21
+ await migrations.init(db);
22
+ }
23
+
24
+ // main is first database
25
+ module.exports.main = module.exports[config.databases[0]];
26
+ };
@@ -0,0 +1,74 @@
1
+
2
+ const _ = require('lodash');
3
+ const mysql = require('mysql2/promise');
4
+ const OPTIONS = [
5
+ 'host', 'port', 'user', 'password', 'database',
6
+ 'charset', 'debug', 'connectionLimit'
7
+ ];
8
+
9
+
10
+ // create pool
11
+ module.exports.createPool = (dbconfig) => {
12
+ return mysql.createPool(_.pick(dbconfig, OPTIONS));
13
+ };
14
+
15
+ // get connection
16
+ module.exports.getConnection = async (pool) => {
17
+ return await pool.getConnection();
18
+ };
19
+
20
+ // query
21
+ module.exports.query = async (connection, sql, params) => {
22
+ return await connection.query(sql, params);
23
+ };
24
+
25
+ // release
26
+ module.exports.release = (connection) => {
27
+ connection.release();
28
+ };
29
+
30
+ // begin transaction
31
+ module.exports.beginTransaction = async (connection) => {
32
+ return await connection.beginTransaction();
33
+ };
34
+
35
+ // commit transaction
36
+ module.exports.commit = async (connection) => {
37
+ return await connection.commit();
38
+ };
39
+
40
+ // rollback transaction
41
+ module.exports.rollback = async (connection) => {
42
+ return await connection.rollback();
43
+ };
44
+
45
+ // dialect
46
+ module.exports.dialect = {
47
+ createDb: db => `CREATE DATABASE \`${db}\`;`,
48
+ dropDb: db => `DROP DATABASE IF EXISTS \`${db}\`;`,
49
+ createMigrationsTable: `CREATE TABLE IF NOT EXISTS \`__db_migrations\`(
50
+ \`id\` INTEGER NOT NULL AUTO_INCREMENT,
51
+ \`file\` VARCHAR(100),
52
+ \`success\` TINYINT(1),
53
+ \`err\` VARCHAR(255),
54
+ \`creation\` DATETIME,
55
+ PRIMARY KEY (\`id\`)
56
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`,
57
+ listMigrations: 'SELECT * FROM `__db_migrations` ORDER BY `id` DESC',
58
+ findMigration: 'SELECT `id` from `__db_migrations` WHERE `file`=? AND `success`=1',
59
+ insertMigration: 'INSERT INTO `__db_migrations` (file, success, err, creation) VALUES(?, ?, ?, ?)',
60
+ esc: '`',
61
+ param: () => '?',
62
+ limit: () => 'LIMIT ?, ? ',
63
+ returning: '',
64
+ insertId: result => result && result.insertId,
65
+ getRows: result => result[0],
66
+ emptyInsert: null,
67
+ in: 'IN',
68
+ notin: 'NOT IN',
69
+ getLock: lock => `SELECT GET_LOCK('${lock}', 0) AS 'lock'`,
70
+ gotLock: res => res && res[0] && res[0][0] && res[0][0].lock > 0,
71
+ releaseLock: lock => `SELECT RELEASE_LOCK('${lock}')`,
72
+
73
+
74
+ };
@@ -0,0 +1,70 @@
1
+
2
+ const { Pool } = require('pg');
3
+
4
+
5
+ // create pool
6
+ module.exports.createPool = (dbconfig) => {
7
+ return new Pool(dbconfig);
8
+ };
9
+
10
+ // get connection
11
+ module.exports.getConnection = async (pool) => {
12
+ return await pool.connect();
13
+ };
14
+
15
+ // query
16
+ module.exports.query = async (connection, sql, params) => {
17
+ return await connection.query(sql, params);
18
+ };
19
+
20
+ // release
21
+ module.exports.release = (connection) => {
22
+ connection.release();
23
+ };
24
+
25
+ // begin transaction
26
+ module.exports.beginTransaction = async (connection) => {
27
+ return await connection.query('BEGIN');
28
+ };
29
+
30
+ // commit transaction
31
+ module.exports.commit = async (connection) => {
32
+ return await connection.query('COMMIT');
33
+ };
34
+
35
+ // rollback transaction
36
+ module.exports.rollback = async (connection) => {
37
+ return await connection.query('ROLLBACK');
38
+ };
39
+
40
+ // dialect
41
+ module.exports.dialect = {
42
+ createDb: db => `CREATE DATABASE "${db}";`,
43
+ dropDb: db => `DROP DATABASE IF EXISTS "${db}";`,
44
+ createMigrationsTable: `CREATE TABLE IF NOT EXISTS "__db_migrations"(
45
+ "id" SERIAL,
46
+ "file" VARCHAR(100),
47
+ "success" BOOLEAN,
48
+ "err" VARCHAR(255),
49
+ "creation" TIMESTAMP,
50
+ PRIMARY KEY ("id")
51
+ );`,
52
+ listMigrations: 'SELECT * FROM "__db_migrations" ORDER BY "id" DESC',
53
+ findMigration: 'SELECT "id" from "__db_migrations" WHERE "file"=$1 AND "success"=TRUE',
54
+ insertMigration: 'INSERT INTO "__db_migrations" (file, success, err, creation) VALUES($1, $2, $3, $4)',
55
+ esc: '"',
56
+ param: i => `$${i}`,
57
+ limit: (i, j) => `LIMIT $${j} OFFSET $${i} `,
58
+ returning: 'RETURNING "id"',
59
+ insertId: result => {
60
+ return result && result[0] && result[0].id;
61
+ },
62
+ getRows: result => result && result.rows,
63
+ emptyInsert: 'DEFAULT VALUES ',
64
+ in: '= ANY',
65
+ notin: '!= ALL',
66
+ getLock: lock => `SELECT pg_try_advisory_lock(hashtext('${lock}')::bigint);`,
67
+ gotLock: res => res && res.rows && res.rows[0] && res.rows[0].pg_try_advisory_lock,
68
+ releaseLock: lock => `SELECT pg_advisory_unlock(hashtext('${lock}')::bigint);`,
69
+
70
+ };
@@ -0,0 +1,140 @@
1
+
2
+ const _ = require('lodash');
3
+
4
+ const fs = require('fs/promises');
5
+ const path = require('path');
6
+
7
+ const context = require('./context');
8
+
9
+ //
10
+ module.exports.init = async (db) => {
11
+ const { config } = context;
12
+ if (!config.auto_migrate) return;
13
+
14
+ try {
15
+ const { connection } = await db.getConnection();
16
+ const { dialect } = db.driver;
17
+ const lock = db.config.database + '.__db_migrations';
18
+ const getLock = dialect.getLock(lock);
19
+ const res = await db.driver.query(connection, getLock, []);
20
+ if (!dialect.gotLock(res)) {
21
+ // could not get lock, skip migration
22
+ return db.driver.release(connection);
23
+ }
24
+
25
+ // got lock, migrate!
26
+ await module.exports.migrate(db);
27
+
28
+ // release lock
29
+ setTimeout(async () => {
30
+ const releaseLock = dialect.releaseLock(lock);
31
+ await db.driver.query(connection, releaseLock);
32
+ db.driver.release(connection);
33
+ }, 10000);
34
+
35
+ } catch (err) {
36
+ console.error(err);
37
+ }
38
+ };
39
+
40
+
41
+ //
42
+ module.exports.initmigrations = async (db) => {
43
+ const sql = db.driver.dialect.createMigrationsTable;
44
+ await db.query(sql);
45
+ };
46
+
47
+ //
48
+ module.exports.list = async (db) => {
49
+ await module.exports.initmigrations(db);
50
+ const sql = db.driver.dialect.listMigrations;
51
+ return await db.query(sql);
52
+ };
53
+
54
+ //
55
+ module.exports.migrate = async (db, rootDir = '.') => {
56
+ const { config, logger } = context;
57
+
58
+ if (db.config.noMigrations) {
59
+ return;
60
+ }
61
+
62
+ const sqldir = `${rootDir}/${db.config.migrations_dir || 'sql'}`;
63
+ let querybuf = '';
64
+
65
+ // create directory if it does not exist
66
+ await fs.mkdir(sqldir, { recursive: true });
67
+
68
+ // execute SQL line
69
+ const executeLine = async (line) => {
70
+ line = line.replace('\r', '').trim();
71
+ if (line.match('^--')) return;
72
+ if (line.match('\\;$')) {
73
+ querybuf += line;
74
+ if (config.mysql && config.mysql.debugsql) logger.info(querybuf);
75
+ await db.query(querybuf);
76
+ querybuf = '';
77
+ } else if (line.length > 0) {
78
+ querybuf += line;
79
+ return;
80
+ } else {
81
+ return;
82
+ }
83
+ };
84
+
85
+ // execute SQL migration file
86
+ const executeFile = async (file) => {
87
+ // Skip hidden files silently (like .gitkeep)
88
+ if (file.filename.startsWith('.')) {
89
+ return;
90
+ }
91
+
92
+ if (!file.filename.match('[0-9]{8}.*\\.sql$')) {
93
+ logger.warn('File %s does not match migration pattern, skipping', file.filename);
94
+ return;
95
+ }
96
+
97
+ // find if file has already been played
98
+ const result = await db.query(db.driver.dialect.findMigration, [file.filename]);
99
+ if (result && result.length > 0) {
100
+ return 'alreadyplayed';
101
+ }
102
+
103
+ try {
104
+ const data = await fs.readFile(file.path);
105
+ const lines = data.toString().split('\n');
106
+ if (config.mysql && config.mysql.debugsql) {
107
+ logger.info('Executing ' + file.path + ': ' + lines.length + ' lines to process');
108
+ }
109
+ for (const line of lines) {
110
+ await executeLine(line);
111
+ }
112
+
113
+ // no error
114
+ await db.query(db.driver.dialect.insertMigration, [file.filename, true, null, new Date()]);
115
+ logger.info('✅ ' + file.filename);
116
+
117
+ } catch (err) {
118
+ await db.query(db.driver.dialect.insertMigration, [file.filename, false, String(err), new Date()]);
119
+ logger.info('❌ ' + file.filename);
120
+ logger.error('SQL error in file %s', file.path);
121
+ throw err;
122
+ }
123
+ }
124
+
125
+ let files = [];
126
+ const filenames = await fs.readdir(sqldir);
127
+ _.forEach(filenames, (filename) => {
128
+ files.push({ filename, path: path.join(sqldir, filename) });
129
+ })
130
+
131
+ files = _.sortBy(files, 'filename');
132
+ await module.exports.initmigrations(db);
133
+ try {
134
+ for (const file of files) {
135
+ await executeFile(file);
136
+ }
137
+ } catch (err) {
138
+ logger.error(err);
139
+ }
140
+ };