@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/index.js ADDED
@@ -0,0 +1,27 @@
1
+
2
+ const context = require('./src/context');
3
+
4
+ // Initialize @igojs/db with dependencies from @igojs/server
5
+ function init({ config, cache, logger, utils, errorhandler }) {
6
+ context.config = config;
7
+ context.cache = cache;
8
+ context.logger = logger;
9
+ context.utils = utils;
10
+ context.errorhandler = errorhandler;
11
+ }
12
+
13
+ module.exports = {
14
+ init,
15
+ get Model() { return require('./src/Model'); },
16
+ get Query() { return require('./src/Query'); },
17
+ get CachedQuery() { return require('./src/CachedQuery'); },
18
+ get Schema() { return require('./src/Schema'); },
19
+ get Sql() { return require('./src/Sql'); },
20
+ get Db() { return require('./src/Db'); },
21
+ get dbs() { return require('./src/dbs'); },
22
+ get migrations() { return require('./src/migrations'); },
23
+ get DataTypes() { return require('./src/DataTypes'); },
24
+ get CacheStats() { return require('./src/CacheStats'); },
25
+ get PaginatedOptimizedQuery() { return require('./src/PaginatedOptimizedQuery'); },
26
+ get PaginatedOptimizedSql() { return require('./src/PaginatedOptimizedSql'); },
27
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@igojs/db",
3
+ "version": "6.0.0-beta.1",
4
+ "description": "Igo ORM - Database abstraction layer for MySQL and PostgreSQL",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha --exit 'test/**/*.js'"
8
+ },
9
+ "keywords": [
10
+ "orm",
11
+ "mysql",
12
+ "postgresql",
13
+ "database"
14
+ ],
15
+ "author": "@igocreate",
16
+ "license": "ISC",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "lodash": "^4.17.21"
22
+ },
23
+ "optionalDependencies": {
24
+ "mysql2": "^3.15.3",
25
+ "pg": "^8.16.3"
26
+ }
27
+ }
@@ -0,0 +1,33 @@
1
+ const _ = require('lodash');
2
+ const context = require('./context');
3
+
4
+ const NAMESPACE = '_cache_statistics';
5
+
6
+ //
7
+ module.exports.incr = (key, type) => {
8
+ const { cache } = context;
9
+ cache.incr(NAMESPACE, `${key}.${type}`);
10
+ };
11
+
12
+ //
13
+ module.exports.getStats = async () => {
14
+ const { cache } = context;
15
+ const statistics = {};
16
+
17
+ await cache.scan(`${NAMESPACE}/*`, async (key) => {
18
+ key = key.substr(NAMESPACE.length + 1);
19
+ const value = await cache.get(NAMESPACE, key);
20
+ _.set(statistics, key, value);
21
+ });
22
+
23
+ _.each(statistics, (statistic, key) => {
24
+ let { hits, misses } = statistic;
25
+ hits = hits || 0;
26
+ misses = misses || 0;
27
+ statistic.table = key;
28
+ statistic.total = hits + misses;
29
+ statistic.rate = Math.round(hits / statistic.total * 100);
30
+ });
31
+
32
+ return _.values(statistics);
33
+ };
@@ -0,0 +1,40 @@
1
+
2
+ const context = require('./context');
3
+
4
+ const Query = require('./Query');
5
+ const CacheStats = require('./CacheStats');
6
+
7
+ //
8
+ module.exports = class CachedQuery extends Query {
9
+
10
+ async runQuery() {
11
+ const { cache } = context;
12
+ const { query, schema } = this;
13
+ const sqlQuery = this.toSQL();
14
+ const db = this.getDb();
15
+
16
+ const namespace = '_cached.' + query.table;
17
+
18
+ if (query.verb !== 'select') {
19
+ cache.flush(namespace + '/*'); // non-blocking flush for performance
20
+ const result = await db.query(sqlQuery.sql, sqlQuery.params, query.options);
21
+ return result;
22
+ }
23
+
24
+ const key = JSON.stringify(sqlQuery);
25
+ let type = 'hits';
26
+
27
+ const result = await cache.fetch(
28
+ namespace,
29
+ key,
30
+ async () => {
31
+ type = 'misses';
32
+ return await db.query(sqlQuery.sql, sqlQuery.params, query.options);
33
+ },
34
+ schema.cache.ttl
35
+ );
36
+
37
+ CacheStats.incr(query.table, type);
38
+ return result;
39
+ }
40
+ };
@@ -0,0 +1,23 @@
1
+
2
+ const _ = require('lodash');
3
+ const context = require('./context');
4
+
5
+
6
+ module.exports = {
7
+ default: {
8
+ set: _.identity,
9
+ get: _.identity,
10
+ },
11
+ boolean: {
12
+ set: value => (value === null || value === undefined) ? null : !!value,
13
+ get: value => (value === null || value === undefined) ? null : !!value,
14
+ },
15
+ json: {
16
+ set: value => context.utils.toJSON(value),
17
+ get: value => context.utils.fromJSON(value),
18
+ },
19
+ array: {
20
+ set: value => value && Array.isArray(value) ? value.join(',') : value,
21
+ get: value => value && value.split ? value.split(',') : []
22
+ },
23
+ };
package/src/Db.js ADDED
@@ -0,0 +1,147 @@
1
+
2
+ const _ = require('lodash');
3
+ const context = require('./context');
4
+
5
+ // Dynamic driver loading
6
+ let loadedDriver = null;
7
+ const getDriver = (driverName) => {
8
+ if (!loadedDriver) {
9
+ loadedDriver = require(`./drivers/${driverName}`);
10
+ }
11
+ return loadedDriver;
12
+ };
13
+
14
+ //
15
+ const logQuery = (sql, params, err) => {
16
+ const { logger, errorhandler } = context;
17
+ const _log = err ? logger.error : logger.info;
18
+ _log('Db.query: ' + sql);
19
+ if (params?.length) {
20
+ _log('With params: ' + params);
21
+ }
22
+ if (err) {
23
+ errorhandler.errorSQL(err);
24
+ }
25
+ }
26
+
27
+
28
+ //
29
+ class Db {
30
+
31
+ constructor(name) {
32
+ const { config } = context;
33
+ this.name = name;
34
+ this.config = config[name];
35
+ this.driver = getDriver(this.config.driver);
36
+ this.connection = null;
37
+ this.config.migrations_dir = `sql/${this.name}`;
38
+ }
39
+
40
+ async init() {
41
+ const { config } = context;
42
+ this.pool = await this.driver.createPool(this.config);
43
+ this.connection = null;
44
+ this.TEST_ENV = config.env === 'test';
45
+ }
46
+
47
+ //
48
+ async getConnection() {
49
+ const { driver, pool, TEST_ENV } = this;
50
+ // if connection is in local storage
51
+ if (TEST_ENV && this.connection) {
52
+ // console.log('keep same connection');
53
+ return { connection: this.connection, keep: true };
54
+ }
55
+
56
+ const connection = await driver.getConnection(pool);
57
+
58
+ if (TEST_ENV) {
59
+ this.connection = connection;
60
+ }
61
+ return { connection, keep: false };
62
+
63
+ }
64
+
65
+ //
66
+ async query(sql, params=[], options={}) {
67
+ const { logger } = context;
68
+ const { driver, config, TEST_ENV } = this;
69
+ const { dialect } = driver;
70
+
71
+ const runquery = async() => {
72
+
73
+ const { connection, keep } = await this.getConnection();
74
+
75
+ try {
76
+
77
+ const result = await this.driver.query(connection, sql, params, options);
78
+ if (config.debugsql) {
79
+ logQuery(sql, params);
80
+ }
81
+ return dialect.getRows(result);
82
+
83
+ } catch (err) {
84
+ if (options.silent) {
85
+ return;
86
+ }
87
+ // log & rethrow error
88
+ logQuery(sql, params, err);
89
+ throw err;
90
+
91
+ } finally {
92
+ if (!keep) {
93
+ // console.log('query: release transaction');
94
+ driver.release(connection);
95
+ if (TEST_ENV) {
96
+ this.connection = null;
97
+ }
98
+ }
99
+ }
100
+ };
101
+
102
+ if (this.pool) {
103
+ return await runquery();
104
+ }
105
+
106
+ logger.info('Db.query: Trying to reinitialize db connection pool');
107
+ await this.init();
108
+ if (!this.pool) {
109
+ logger.error('could not create db connection pool');
110
+ } else {
111
+ return await runquery();
112
+ }
113
+ }
114
+
115
+
116
+
117
+ //
118
+ async beginTransaction() {
119
+ const { driver } = this;
120
+ const { connection } = await this.getConnection();
121
+ await driver.beginTransaction(connection);
122
+ }
123
+
124
+ //
125
+ async commitTransaction() {
126
+ const { driver, TEST_ENV } = this;
127
+ const { connection } = await this.getConnection();
128
+ await driver.commit(connection);
129
+ driver.release(connection);
130
+ if (TEST_ENV) {
131
+ this.connection = null;
132
+ }
133
+ }
134
+
135
+ //
136
+ async rollbackTransaction() {
137
+ const { driver, TEST_ENV } = this;
138
+ const { connection } = await this.getConnection();
139
+ await driver.rollback(connection);
140
+ driver.release(connection);
141
+ if (TEST_ENV) {
142
+ this.connection = null;
143
+ }
144
+ }
145
+ }
146
+
147
+ module.exports = Db;
package/src/Model.js ADDED
@@ -0,0 +1,261 @@
1
+ const _ = require('lodash');
2
+ const CachedQuery = require('./CachedQuery');
3
+
4
+ const Query = require('./Query');
5
+ const PaginatedOptimizedQuery = require('./PaginatedOptimizedQuery');
6
+ const Schema = require('./Schema');
7
+ const context = require('./context');
8
+
9
+
10
+ const newQuery = (constructor, verb) => {
11
+ if (constructor.schema.cache) {
12
+ return new CachedQuery(constructor, verb);
13
+ }
14
+ return new Query(constructor, verb);
15
+ };
16
+
17
+ // Simple mixin implementation to set the schema as a static attribute
18
+ module.exports = function(schema) {
19
+
20
+ class Model {
21
+
22
+ constructor(values) {
23
+ _.assign(this, values);
24
+ }
25
+
26
+ //
27
+ assignValues(values) {
28
+ const keys = _.keys(Model.schema.colsByAttr);
29
+ _.assign(this, _.pick(values, keys));
30
+ }
31
+
32
+ // returns object with primary keys
33
+ primaryObject() {
34
+ return _.pick(this, this.constructor.schema.primary);
35
+ }
36
+
37
+ // update
38
+ async update(values) {
39
+ values.updated_at = new Date();
40
+ await this.beforeUpdate(values);
41
+
42
+ await newQuery(this.constructor, 'update')
43
+ .unscoped()
44
+ .values(values)
45
+ .where(this.primaryObject())
46
+ .execute();
47
+
48
+ if (this.constructor.schema.cache) {
49
+ const { cache } = context;
50
+ await cache.del('_cached.' + this.constructor.schema.table);
51
+ }
52
+
53
+ this.assignValues(values);
54
+ return this;
55
+ }
56
+
57
+ // reload
58
+ async reload(includes) {
59
+ const query = this.constructor.unscoped();
60
+ includes && query.includes(includes);
61
+ return await query.find(this.id);
62
+ }
63
+
64
+ // delete
65
+ delete() {
66
+ return newQuery(this.constructor, 'delete').unscoped().where(this.primaryObject()).execute();
67
+ }
68
+
69
+ async beforeCreate() { }
70
+ async beforeUpdate(values) { }
71
+
72
+
73
+ // find by id
74
+ static async find(id) {
75
+ return await newQuery(this).find(id);
76
+ }
77
+
78
+ // create
79
+ static async create(values, options) {
80
+ const _this = this;
81
+
82
+ const now = new Date();
83
+ const obj = new this(values);
84
+
85
+ if (this.schema.subclasses && !obj[this.schema.subclass_column]) {
86
+ obj[this.schema.subclass_column] = _.findKey(this.schema.subclasses, { name: this.name });
87
+ }
88
+
89
+ obj.created_at = obj.created_at || now;
90
+ obj.updated_at = obj.updated_at || now;
91
+
92
+ const create = async () => {
93
+ await obj.beforeCreate();
94
+
95
+ const query = newQuery(_this, 'insert').values(obj).options(options);
96
+ const result = await query.execute();
97
+
98
+ if (result.err) {
99
+ throw result.err;
100
+ }
101
+
102
+ const { insertId } = result;
103
+ if (insertId) {
104
+ return _this.unscoped().find(insertId);
105
+ }
106
+ return _this.unscoped().find(obj.primaryObject());
107
+ };
108
+
109
+ return await create();
110
+ }
111
+
112
+
113
+ // return first
114
+ static first() {
115
+ return newQuery(this).first();
116
+ }
117
+
118
+ // return last
119
+ static last() {
120
+ return newQuery(this).last();
121
+ }
122
+
123
+ // return list
124
+ static list() {
125
+ return newQuery(this).list();
126
+ }
127
+
128
+ //
129
+ static select(select) {
130
+ return newQuery(this).select(select);
131
+ }
132
+
133
+ // filter
134
+ static where(where, params) {
135
+ return newQuery(this).where(where, params);
136
+ }
137
+
138
+ // filter
139
+ static whereNot(whereNot) {
140
+ return newQuery(this).whereNot(whereNot);
141
+ }
142
+
143
+ // limit
144
+ static limit(limit) {
145
+ return newQuery(this).limit(limit);
146
+ }
147
+
148
+ // offset
149
+ static offset(offset) {
150
+ return newQuery(this).offset(offset);
151
+ }
152
+
153
+ // page
154
+ static page(page, nb) {
155
+ return newQuery(this).page(page, nb);
156
+ }
157
+
158
+ // order
159
+ static order(order) {
160
+ return newQuery(this).order(order);
161
+ }
162
+
163
+ // distinct
164
+ static distinct(columns) {
165
+ return newQuery(this).distinct(columns);
166
+ }
167
+
168
+ // group
169
+ static group(columns) {
170
+ return newQuery(this).group(columns);
171
+ }
172
+
173
+ // count
174
+ static count() {
175
+ return newQuery(this).count();
176
+ }
177
+
178
+ // delete
179
+ static delete(id, ) {
180
+ return newQuery(this, 'delete').unscoped().where({ id: id }).execute();
181
+ }
182
+
183
+ // delete all
184
+ static async deleteAll() {
185
+ return newQuery(this, 'delete').unscoped().execute();
186
+ }
187
+
188
+ // destroy all
189
+ static destroyAll() {
190
+ return this.deleteAll();
191
+ }
192
+
193
+ //
194
+ static update(values, ) {
195
+ values.updated_at = new Date();
196
+ return newQuery(this).unscoped().update(values, );
197
+ }
198
+
199
+ // includes
200
+ static includes(includes) {
201
+ return newQuery(this).includes(includes);
202
+ }
203
+
204
+ // includes
205
+ static join(associationName, columns, type='LEFT') {
206
+ const query = newQuery(this);
207
+ if (_.isString(associationName)) {
208
+ return query.joinOne(associationName, columns, type);
209
+ } else if (_.isArray(associationName)) {
210
+ return query.joinMany(associationName, columns, type);
211
+ } else if (_.isObject(associationName)) {
212
+ return query.joinNested(associationName);
213
+ }
214
+ console.warn('Invalid join argument for Model.join(). Must be a string, array, or object.');
215
+ }
216
+
217
+ //unscoped
218
+ static unscoped() {
219
+ return newQuery(this).unscoped();
220
+ }
221
+
222
+ //scope
223
+ static scope(scope) {
224
+ return newQuery(this).scope(scope);
225
+ }
226
+
227
+ /**
228
+ * paginatedOptimized - Retourne une PaginatedOptimizedQuery pour des requêtes optimisées avec pattern COUNT/IDS/FULL
229
+ *
230
+ * Cette méthode permet d'utiliser le pattern d'optimisation pour les requêtes avec de nombreuses jointures.
231
+ * Au lieu de faire un LEFT JOIN complet, on utilise :
232
+ * 1. COUNT avec EXISTS pour compter sans jointures
233
+ * 2. SELECT IDS pour récupérer les IDs avec filtres/tris/pagination
234
+ * 3. SELECT FULL pour récupérer les données complètes avec LEFT JOIN uniquement sur les IDs trouvés
235
+ *
236
+ * @returns {PaginatedOptimizedQuery} Instance de PaginatedOptimizedQuery
237
+ *
238
+ * Exemple d'utilisation :
239
+ *
240
+ * const result = await Folder.paginatedOptimized()
241
+ * .where({ type: ['agp', 'avt'] })
242
+ * .filterJoin('applicant', { last_name: 'Dupont%' }) // Filtre via EXISTS
243
+ * .join('pme_folder') // LEFT JOIN dans phase FULL
244
+ * .order('folders.created_at DESC')
245
+ * .page(1, 50);
246
+ *
247
+ * Performance :
248
+ * - Pour des tables de millions de lignes avec 10 jointures : amélioration de 1000x à 10000x
249
+ * - Le COUNT passe de plusieurs secondes à quelques millisecondes
250
+ * - Le SELECT bénéficie d'une pagination efficace sur les IDs seulement
251
+ */
252
+ static paginatedOptimized() {
253
+ return new PaginatedOptimizedQuery(this);
254
+ }
255
+
256
+ }
257
+
258
+ Model.schema = new Schema(schema);
259
+
260
+ return Model;
261
+ };