@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/README.md +153 -0
- package/examples/PaginatedOptimizedQueryExample.js +936 -0
- package/index.js +27 -0
- package/package.json +27 -0
- package/src/CacheStats.js +33 -0
- package/src/CachedQuery.js +40 -0
- package/src/DataTypes.js +23 -0
- package/src/Db.js +147 -0
- package/src/Model.js +261 -0
- package/src/PaginatedOptimizedQuery.js +902 -0
- package/src/PaginatedOptimizedSql.js +1352 -0
- package/src/Query.js +584 -0
- package/src/Schema.js +52 -0
- package/src/Sql.js +311 -0
- package/src/context.js +12 -0
- package/src/dbs.js +26 -0
- package/src/drivers/mysql.js +74 -0
- package/src/drivers/postgresql.js +70 -0
- package/src/migrations.js +140 -0
- package/test/AssociationsTest.js +301 -0
- package/test/CacheStatsTest.js +40 -0
- package/test/CachedQueryTest.js +49 -0
- package/test/JoinTest.js +207 -0
- package/test/ModelTest.js +510 -0
- package/test/PaginatedOptimizedQueryTest.js +1183 -0
- package/test/PerfTest.js +58 -0
- package/test/PostgreSqlTest.js +95 -0
- package/test/QueryTest.js +27 -0
- package/test/SimplifiedSyntaxTest.js +473 -0
- package/test/SqlTest.js +95 -0
- package/test/init.js +2 -0
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
|
+
};
|