millas 0.1.8 → 0.1.9
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/package.json +1 -1
- package/src/index.js +10 -2
- package/src/orm/index.js +19 -0
- package/src/orm/model/Model.js +398 -133
- package/src/orm/query/Aggregates.js +56 -0
- package/src/orm/query/LookupParser.js +308 -0
- package/src/orm/query/Q.js +123 -0
- package/src/orm/query/QueryBuilder.js +266 -82
- package/src/orm/relations/BelongsTo.js +68 -0
- package/src/orm/relations/BelongsToMany.js +188 -0
- package/src/orm/relations/HasMany.js +72 -0
- package/src/orm/relations/HasOne.js +67 -0
- package/src/orm/relations/index.js +8 -0
- package/src/scaffold/maker.js +39 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Aggregates
|
|
5
|
+
*
|
|
6
|
+
* Functions for use with QueryBuilder.aggregate() and .annotate().
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { Sum, Avg, Min, Max, Count } = require('millas/src/orm/query/Aggregates');
|
|
10
|
+
*
|
|
11
|
+
* // Single aggregate value
|
|
12
|
+
* await Order.aggregate({ total: Sum('amount') })
|
|
13
|
+
* // → { total: 9430.50 }
|
|
14
|
+
*
|
|
15
|
+
* await Order.aggregate({
|
|
16
|
+
* total: Sum('amount'),
|
|
17
|
+
* average: Avg('amount'),
|
|
18
|
+
* lowest: Min('amount'),
|
|
19
|
+
* highest: Max('amount'),
|
|
20
|
+
* orders: Count('id'),
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Per-row annotation (adds a computed column to each result)
|
|
24
|
+
* await Post.annotate({ comment_count: Count('comments.id') })
|
|
25
|
+
* .where('active', true)
|
|
26
|
+
* .get()
|
|
27
|
+
* // each Post instance has .comment_count
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
class AggregateExpression {
|
|
31
|
+
constructor(fn, column, options = {}) {
|
|
32
|
+
this.fn = fn; // 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'COUNT'
|
|
33
|
+
this.column = column;
|
|
34
|
+
this.distinct = options.distinct ?? false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render to a knex raw expression string for use in .select() or .count() etc.
|
|
39
|
+
* @param {string} alias — SQL alias for the result column
|
|
40
|
+
*/
|
|
41
|
+
toSQL(alias) {
|
|
42
|
+
const col = this.column === '*' ? '*' : `"${this.column.replace('.', '"."')}"`;
|
|
43
|
+
const expr = this.distinct ? `${this.fn}(DISTINCT ${col})` : `${this.fn}(${col})`;
|
|
44
|
+
return alias ? `${expr} as ${alias}` : expr;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Factory helpers ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const Sum = (column, opts) => new AggregateExpression('SUM', column, opts);
|
|
51
|
+
const Avg = (column, opts) => new AggregateExpression('AVG', column, opts);
|
|
52
|
+
const Min = (column, opts) => new AggregateExpression('MIN', column, opts);
|
|
53
|
+
const Max = (column, opts) => new AggregateExpression('MAX', column, opts);
|
|
54
|
+
const Count = (column = '*', opts) => new AggregateExpression('COUNT', column, opts);
|
|
55
|
+
|
|
56
|
+
module.exports = { AggregateExpression, Sum, Avg, Min, Max, Count };
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LookupParser
|
|
5
|
+
*
|
|
6
|
+
* Parses Django-style __ field lookups and applies them to a knex query.
|
|
7
|
+
*
|
|
8
|
+
* Supported lookup types:
|
|
9
|
+
*
|
|
10
|
+
* Comparison
|
|
11
|
+
* field__exact = (default, same as no suffix)
|
|
12
|
+
* field__not !=
|
|
13
|
+
* field__gt >
|
|
14
|
+
* field__gte >=
|
|
15
|
+
* field__lt <
|
|
16
|
+
* field__lte <=
|
|
17
|
+
*
|
|
18
|
+
* Null checks
|
|
19
|
+
* field__isnull IS NULL (value: true) / IS NOT NULL (value: false)
|
|
20
|
+
*
|
|
21
|
+
* Range / set
|
|
22
|
+
* field__in IN (array)
|
|
23
|
+
* field__notin NOT IN (array)
|
|
24
|
+
* field__between BETWEEN [min, max]
|
|
25
|
+
*
|
|
26
|
+
* String matching
|
|
27
|
+
* field__contains LIKE %value% (case-sensitive)
|
|
28
|
+
* field__icontains ILIKE %value% (case-insensitive)
|
|
29
|
+
* field__startswith LIKE value%
|
|
30
|
+
* field__istartswith ILIKE value%
|
|
31
|
+
* field__endswith LIKE %value
|
|
32
|
+
* field__iendswith ILIKE %value
|
|
33
|
+
* field__like LIKE <raw pattern> (you supply the %)
|
|
34
|
+
*
|
|
35
|
+
* Date / time extraction (SQLite: strftime, PG/MySQL: EXTRACT / DATE_FORMAT)
|
|
36
|
+
* field__year
|
|
37
|
+
* field__month
|
|
38
|
+
* field__day
|
|
39
|
+
* field__hour
|
|
40
|
+
* field__minute
|
|
41
|
+
* field__second
|
|
42
|
+
*
|
|
43
|
+
* Relationship traversal (requires Model.fields with references)
|
|
44
|
+
* profile__city__icontains → joins profiles, filters on city
|
|
45
|
+
*
|
|
46
|
+
* Usage (internal — called by QueryBuilder):
|
|
47
|
+
* LookupParser.apply(knexQuery, 'age__gte', 18, ModelClass)
|
|
48
|
+
* LookupParser.apply(knexQuery, 'profile__city', 'Nairobi', ModelClass)
|
|
49
|
+
*/
|
|
50
|
+
class LookupParser {
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* All recognised lookup suffixes, in order from longest to shortest so
|
|
54
|
+
* that e.g. "icontains" is matched before a hypothetical "contains" variant.
|
|
55
|
+
*/
|
|
56
|
+
static LOOKUPS = [
|
|
57
|
+
'icontains', 'istartswith', 'iendswith',
|
|
58
|
+
'contains', 'startswith', 'endswith', 'like',
|
|
59
|
+
'isnull', 'between', 'notin', 'in',
|
|
60
|
+
'exact', 'not',
|
|
61
|
+
'gte', 'lte', 'gt', 'lt',
|
|
62
|
+
'year', 'month', 'day', 'hour', 'minute', 'second',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse a lookup key and apply the appropriate knex constraint.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} q — knex query builder (mutated in place)
|
|
69
|
+
* @param {string} key — e.g. "age__gte", "profile__city__icontains"
|
|
70
|
+
* @param {*} value — the comparison value
|
|
71
|
+
* @param {class} ModelClass — the root Model class (for relationship traversal)
|
|
72
|
+
* @param {string} [method] — 'where' | 'orWhere' (default: 'where')
|
|
73
|
+
* @returns {object} the (mutated) knex query
|
|
74
|
+
*/
|
|
75
|
+
static apply(q, key, value, ModelClass, method = 'where') {
|
|
76
|
+
// No __ at all → plain equality, pass straight through
|
|
77
|
+
if (!key.includes('__')) {
|
|
78
|
+
q[method](key, value);
|
|
79
|
+
return q;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parts = key.split('__');
|
|
83
|
+
const lookup = this._extractLookup(parts);
|
|
84
|
+
// Everything left after stripping the lookup is the field path
|
|
85
|
+
const fieldPath = lookup ? parts.slice(0, -1) : parts;
|
|
86
|
+
|
|
87
|
+
// Relationship traversal: more than one segment in the field path
|
|
88
|
+
if (fieldPath.length > 1) {
|
|
89
|
+
return this._applyRelational(q, fieldPath, lookup, value, ModelClass, method);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const column = fieldPath[0];
|
|
93
|
+
return this._applyLookup(q, column, lookup || 'exact', value, method);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Lookup application ───────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
static _applyLookup(q, column, lookup, value, method) {
|
|
99
|
+
switch (lookup) {
|
|
100
|
+
case 'exact':
|
|
101
|
+
q[method](column, value);
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'not':
|
|
105
|
+
q[method](column, '!=', value);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'gt':
|
|
109
|
+
q[method](column, '>', value);
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'gte':
|
|
113
|
+
q[method](column, '>=', value);
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case 'lt':
|
|
117
|
+
q[method](column, '<', value);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'lte':
|
|
121
|
+
q[method](column, '<=', value);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'isnull':
|
|
125
|
+
if (value) q[`${method}Null`] ? q[`${method}Null`](column) : q.whereNull(column);
|
|
126
|
+
else q[`${method}NotNull`] ? q[`${method}NotNull`](column) : q.whereNotNull(column);
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'in':
|
|
130
|
+
q[`${method}In`] ? q[`${method}In`](column, value) : q.whereIn(column, value);
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'notin':
|
|
134
|
+
q[`${method}NotIn`] ? q[`${method}NotIn`](column, value) : q.whereNotIn(column, value);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'between':
|
|
138
|
+
q[`${method}Between`]
|
|
139
|
+
? q[`${method}Between`](column, value)
|
|
140
|
+
: q.whereBetween(column, value);
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case 'contains':
|
|
144
|
+
q[method](column, 'like', `%${value}%`);
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case 'icontains':
|
|
148
|
+
q[method](column, 'ilike', `%${value}%`);
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'startswith':
|
|
152
|
+
q[method](column, 'like', `${value}%`);
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'istartswith':
|
|
156
|
+
q[method](column, 'ilike', `${value}%`);
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 'endswith':
|
|
160
|
+
q[method](column, 'like', `%${value}`);
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case 'iendswith':
|
|
164
|
+
q[method](column, 'ilike', `%${value}`);
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'like':
|
|
168
|
+
q[method](column, 'like', value);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
// ── Date/time extractions ──────────────────────────────────────────
|
|
172
|
+
case 'year':
|
|
173
|
+
case 'month':
|
|
174
|
+
case 'day':
|
|
175
|
+
case 'hour':
|
|
176
|
+
case 'minute':
|
|
177
|
+
case 'second':
|
|
178
|
+
this._applyDatePart(q, column, lookup, value, method);
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
default:
|
|
182
|
+
// Unrecognised suffix — treat whole key as column name, do equality
|
|
183
|
+
q[method](column, value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return q;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Date part extraction — dialect-aware ─────────────────────────────────
|
|
190
|
+
|
|
191
|
+
static _applyDatePart(q, column, part, value, method) {
|
|
192
|
+
const client = q.client?.config?.client || 'sqlite3';
|
|
193
|
+
|
|
194
|
+
if (client.includes('pg') || client.includes('postgres')) {
|
|
195
|
+
// PostgreSQL: EXTRACT(YEAR FROM column) = value
|
|
196
|
+
const pgPart = part.toUpperCase();
|
|
197
|
+
q[method](q.client.raw(`EXTRACT(${pgPart} FROM "${column}")`, []), value);
|
|
198
|
+
|
|
199
|
+
} else if (client.includes('mysql') || client.includes('maria')) {
|
|
200
|
+
// MySQL: YEAR(column) = value etc.
|
|
201
|
+
const fn = part.toUpperCase();
|
|
202
|
+
q[method](q.client.raw(`${fn}(\`${column}\`)`, []), value);
|
|
203
|
+
|
|
204
|
+
} else {
|
|
205
|
+
// SQLite: strftime('%Y', column) = value
|
|
206
|
+
const fmtMap = {
|
|
207
|
+
year: '%Y', month: '%m', day: '%d',
|
|
208
|
+
hour: '%H', minute: '%M', second: '%S',
|
|
209
|
+
};
|
|
210
|
+
const fmt = fmtMap[part];
|
|
211
|
+
q[method](q.client.raw(`strftime('${fmt}', "${column}")`, []), String(value).padStart(part === 'year' ? 4 : 2, '0'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Relationship traversal ───────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Handle multi-segment paths like profile__city or post__author__role.
|
|
219
|
+
*
|
|
220
|
+
* Resolves each hop using Model.fields[x].references, auto-joins the
|
|
221
|
+
* necessary tables, then applies the final lookup on the leaf column.
|
|
222
|
+
*
|
|
223
|
+
* @param {object} q — knex query
|
|
224
|
+
* @param {string[]} fieldPath — e.g. ['profile', 'city']
|
|
225
|
+
* @param {string} lookup — e.g. 'icontains'
|
|
226
|
+
* @param {*} value
|
|
227
|
+
* @param {class} ModelClass — root model
|
|
228
|
+
* @param {string} method — 'where' | 'orWhere'
|
|
229
|
+
*/
|
|
230
|
+
static _applyRelational(q, fieldPath, lookup, value, ModelClass, method) {
|
|
231
|
+
let currentModel = ModelClass;
|
|
232
|
+
const joinedTables = new Set();
|
|
233
|
+
|
|
234
|
+
// Walk all segments except the last — each one is a relationship hop
|
|
235
|
+
for (let i = 0; i < fieldPath.length - 1; i++) {
|
|
236
|
+
const segment = fieldPath[i];
|
|
237
|
+
const fields = currentModel.fields || {};
|
|
238
|
+
|
|
239
|
+
// Try to find a field whose name matches the segment
|
|
240
|
+
const fieldDef = fields[segment] || fields[`${segment}_id`];
|
|
241
|
+
|
|
242
|
+
if (!fieldDef || !fieldDef.references) {
|
|
243
|
+
// No references info — fall back to treating the whole path as a
|
|
244
|
+
// raw column name with underscores (best-effort)
|
|
245
|
+
const fallbackCol = fieldPath.join('_');
|
|
246
|
+
return this._applyLookup(q, fallbackCol, lookup || 'exact', value, method);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { table: relTable, column: relColumn } = fieldDef.references;
|
|
250
|
+
const localColumn = fieldDef.references.localKey || segment + '_id';
|
|
251
|
+
const joinKey = `${currentModel.table}__${relTable}`;
|
|
252
|
+
|
|
253
|
+
if (!joinedTables.has(joinKey)) {
|
|
254
|
+
q.join(relTable, `${currentModel.table}.${localColumn}`, '=', `${relTable}.${relColumn}`);
|
|
255
|
+
joinedTables.add(joinKey);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Advance — try to resolve the related model for the next hop.
|
|
259
|
+
// We do a best-effort require() from the models directory.
|
|
260
|
+
currentModel = this._resolveRelatedModel(relTable) || { table: relTable, fields: {} };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The final segment is the leaf column on the last joined table
|
|
264
|
+
const leafColumn = `${currentModel.table}.${fieldPath[fieldPath.length - 1]}`;
|
|
265
|
+
return this._applyLookup(q, leafColumn, lookup || 'exact', value, method);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Try to resolve a related Model class by table name.
|
|
270
|
+
* Looks in process.cwd()/app/models/ (best-effort, graceful failure).
|
|
271
|
+
*/
|
|
272
|
+
static _resolveRelatedModel(tableName) {
|
|
273
|
+
try {
|
|
274
|
+
const path = require('path');
|
|
275
|
+
const fs = require('fs');
|
|
276
|
+
const modelsDir = path.join(process.cwd(), 'app', 'models');
|
|
277
|
+
|
|
278
|
+
if (!fs.existsSync(modelsDir)) return null;
|
|
279
|
+
|
|
280
|
+
const files = fs.readdirSync(modelsDir).filter(f => f.endsWith('.js'));
|
|
281
|
+
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
try {
|
|
284
|
+
const cls = require(path.join(modelsDir, file));
|
|
285
|
+
const ModelClass = typeof cls === 'function' ? cls
|
|
286
|
+
: Object.values(cls).find(v => typeof v === 'function');
|
|
287
|
+
|
|
288
|
+
if (ModelClass && ModelClass.table === tableName) return ModelClass;
|
|
289
|
+
} catch { /* skip */ }
|
|
290
|
+
}
|
|
291
|
+
} catch { /* skip */ }
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Given the split parts array, return the lookup suffix if the last
|
|
300
|
+
* element is a known lookup keyword, otherwise return null.
|
|
301
|
+
*/
|
|
302
|
+
static _extractLookup(parts) {
|
|
303
|
+
const last = parts[parts.length - 1];
|
|
304
|
+
return this.LOOKUPS.includes(last) ? last : null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = LookupParser;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LookupParser = require('./LookupParser');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Q — complex boolean query objects
|
|
7
|
+
*
|
|
8
|
+
* Lets you express any combination of AND / OR / NOT conditions,
|
|
9
|
+
* including deeply nested groups, in a single readable expression.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const { Q } = require('millas');
|
|
13
|
+
*
|
|
14
|
+
* // OR
|
|
15
|
+
* User.filter(Q({ age__gte: 18 }).or(Q({ role: 'admin' })))
|
|
16
|
+
*
|
|
17
|
+
* // AND (default when chaining .where())
|
|
18
|
+
* User.filter(Q({ city: 'Nairobi' }).and(Q({ active: true })))
|
|
19
|
+
*
|
|
20
|
+
* // NOT
|
|
21
|
+
* User.filter(Q({ status: 'banned' }).not())
|
|
22
|
+
*
|
|
23
|
+
* // Nested
|
|
24
|
+
* User.filter(
|
|
25
|
+
* Q({ role: 'admin' }).or(
|
|
26
|
+
* Q({ age__gte: 18 }).and(Q({ active: true }))
|
|
27
|
+
* )
|
|
28
|
+
* )
|
|
29
|
+
*
|
|
30
|
+
* // Shorthand operators
|
|
31
|
+
* Q({ age__gte: 18 }).or({ role: 'admin' })
|
|
32
|
+
*/
|
|
33
|
+
class Q {
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} conditions — plain { field: value } or { field__lookup: value }
|
|
36
|
+
*/
|
|
37
|
+
constructor(conditions = {}) {
|
|
38
|
+
this._conditions = conditions;
|
|
39
|
+
this._children = []; // [{ type: 'and'|'or', node: Q }]
|
|
40
|
+
this._negated = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Combinators ──────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
and(other) {
|
|
46
|
+
const node = other instanceof Q ? other : new Q(other);
|
|
47
|
+
this._children.push({ type: 'and', node });
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
or(other) {
|
|
52
|
+
const node = other instanceof Q ? other : new Q(other);
|
|
53
|
+
this._children.push({ type: 'or', node });
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
not() {
|
|
58
|
+
this._negated = !this._negated;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Application ──────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Apply this Q tree to a knex query builder.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} knexQuery — knex query (mutated)
|
|
68
|
+
* @param {class} ModelClass — root model (for lookups / joins)
|
|
69
|
+
* @param {string} method — 'where' | 'orWhere'
|
|
70
|
+
*/
|
|
71
|
+
apply(knexQuery, ModelClass, method = 'where') {
|
|
72
|
+
const applyFn = this._negated ? `${method}Not` : method;
|
|
73
|
+
|
|
74
|
+
knexQuery[method](function () {
|
|
75
|
+
const sub = this; // knex sub-query builder
|
|
76
|
+
|
|
77
|
+
// Apply own conditions
|
|
78
|
+
for (const [key, value] of Object.entries(Q.prototype._conditions
|
|
79
|
+
? {}
|
|
80
|
+
: {})) { void key; void value; } // placeholder — see below
|
|
81
|
+
|
|
82
|
+
// Own conditions
|
|
83
|
+
for (const [key, value] of Object.entries(
|
|
84
|
+
// `this` in constructor is the Q instance captured by closure below
|
|
85
|
+
{}
|
|
86
|
+
)) { void key; void value; }
|
|
87
|
+
|
|
88
|
+
// We need the Q instance inside the knex callback — use a wrapper
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Re-implement without the confusing closure issue:
|
|
92
|
+
this._applyToBuilder(knexQuery, ModelClass, method);
|
|
93
|
+
return knexQuery;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Internal — recursively applies the Q tree.
|
|
98
|
+
*/
|
|
99
|
+
_applyToBuilder(knexQuery, ModelClass, outerMethod = 'where') {
|
|
100
|
+
const self = this;
|
|
101
|
+
const wrapped = this._negated ? `${outerMethod}Not` : outerMethod;
|
|
102
|
+
|
|
103
|
+
// knex `.where(function() { ... })` creates a grouped sub-expression
|
|
104
|
+
knexQuery[outerMethod](function () {
|
|
105
|
+
const sub = this;
|
|
106
|
+
|
|
107
|
+
// Own conditions
|
|
108
|
+
for (const [key, value] of Object.entries(self._conditions)) {
|
|
109
|
+
LookupParser.apply(sub, key, value, ModelClass, 'where');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Child nodes
|
|
113
|
+
for (const { type, node } of self._children) {
|
|
114
|
+
const childMethod = type === 'or' ? 'orWhere' : 'where';
|
|
115
|
+
node._applyToBuilder(sub, ModelClass, childMethod);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return knexQuery;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = Q;
|