UNPKG

24.3 kBJavaScriptView Raw
1"use strict";
2/*
3 * @adonisjs/lucid
4 *
5 * (c) Harminder Virk <virk@adonisjs.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10Object.defineProperty(exports, "__esModule", { value: true });
11exports.ModelQueryBuilder = void 0;
12const utils_1 = require("@poppinss/utils");
13const utils_2 = require("../../utils");
14const Preloader_1 = require("../Preloader");
15const Paginator_1 = require("../Paginator");
16const QueryRunner_1 = require("../../QueryRunner");
17const Chainable_1 = require("../../Database/QueryBuilder/Chainable");
18const SimplePaginator_1 = require("../../Database/Paginator/SimplePaginator");
19/**
20 * A wrapper to invoke scope methods on the query builder
21 * underlying model
22 */
23class ModelScopes {
24 constructor(builder) {
25 Object.defineProperty(this, "builder", {
26 enumerable: true,
27 configurable: true,
28 writable: true,
29 value: builder
30 });
31 return new Proxy(this, {
32 get(target, key) {
33 if (typeof target.builder.model[key] === 'function') {
34 return (...args) => {
35 return target.builder.model[key](target.builder, ...args);
36 };
37 }
38 /**
39 * Unknown keys are not allowed
40 */
41 throw new Error(`"${String(key)}" is not defined as a query scope on "${target.builder.model.name}" model`);
42 },
43 });
44 }
45}
46/**
47 * Database query builder exposes the API to construct and run queries for selecting,
48 * updating and deleting records.
49 */
50class ModelQueryBuilder extends Chainable_1.Chainable {
51 constructor(builder, model, client, customFn = (userFn) => {
52 return ($builder) => {
53 const subQuery = new ModelQueryBuilder($builder, this.model, this.client);
54 subQuery.isChildQuery = true;
55 userFn(subQuery);
56 subQuery.applyWhere();
57 };
58 }) {
59 super(builder, customFn, model.$keys.attributesToColumns.resolve.bind(model.$keys.attributesToColumns));
60 Object.defineProperty(this, "model", {
61 enumerable: true,
62 configurable: true,
63 writable: true,
64 value: model
65 });
66 Object.defineProperty(this, "client", {
67 enumerable: true,
68 configurable: true,
69 writable: true,
70 value: client
71 });
72 /**
73 * Sideloaded attributes that will be passed to the model instances
74 */
75 Object.defineProperty(this, "sideloaded", {
76 enumerable: true,
77 configurable: true,
78 writable: true,
79 value: {}
80 });
81 /**
82 * A copy of defined preloads on the model instance
83 */
84 Object.defineProperty(this, "preloader", {
85 enumerable: true,
86 configurable: true,
87 writable: true,
88 value: new Preloader_1.Preloader(this.model)
89 });
90 /**
91 * A custom callback to transform each model row
92 */
93 Object.defineProperty(this, "rowTransformerCallback", {
94 enumerable: true,
95 configurable: true,
96 writable: true,
97 value: void 0
98 });
99 /**
100 * A references to model scopes wrapper. It is lazily initialized
101 * only when the `apply` method is invoked
102 */
103 Object.defineProperty(this, "scopesWrapper", {
104 enumerable: true,
105 configurable: true,
106 writable: true,
107 value: undefined
108 });
109 /**
110 * Control whether or not to wrap adapter result to model
111 * instances or not
112 */
113 Object.defineProperty(this, "wrapResultsToModelInstances", {
114 enumerable: true,
115 configurable: true,
116 writable: true,
117 value: true
118 });
119 /**
120 * Custom data someone want to send to the profiler and the
121 * query event
122 */
123 Object.defineProperty(this, "customReporterData", {
124 enumerable: true,
125 configurable: true,
126 writable: true,
127 value: void 0
128 });
129 /**
130 * Control whether to debug the query or not. The initial
131 * value is inherited from the query client
132 */
133 Object.defineProperty(this, "debugQueries", {
134 enumerable: true,
135 configurable: true,
136 writable: true,
137 value: this.client.debug
138 });
139 /**
140 * Self join counter, increments with every "withCount"
141 * "has" and "whereHas" queries.
142 */
143 Object.defineProperty(this, "joinCounter", {
144 enumerable: true,
145 configurable: true,
146 writable: true,
147 value: 0
148 });
149 /**
150 * Options that must be passed to all new model instances
151 */
152 Object.defineProperty(this, "clientOptions", {
153 enumerable: true,
154 configurable: true,
155 writable: true,
156 value: {
157 client: this.client,
158 connection: this.client.connectionName,
159 profiler: this.client.profiler,
160 }
161 });
162 /**
163 * Whether or not query is a subquery for `.where` callback
164 */
165 Object.defineProperty(this, "isChildQuery", {
166 enumerable: true,
167 configurable: true,
168 writable: true,
169 value: false
170 });
171 /**
172 * Assign table when not already assigned
173 */
174 if (!builder['_single'] || !builder['_single'].table) {
175 builder.table(model.table);
176 }
177 }
178 /**
179 * Executes the current query
180 */
181 async execQuery() {
182 this.applyWhere();
183 const isWriteQuery = ['update', 'del', 'insert'].includes(this.knexQuery['_method']);
184 const queryData = Object.assign(this.getQueryData(), this.customReporterData);
185 const rows = await new QueryRunner_1.QueryRunner(this.client, this.debugQueries, queryData).run(this.knexQuery);
186 /**
187 * Return the rows as it is when query is a write query
188 */
189 if (isWriteQuery || !this.wrapResultsToModelInstances) {
190 return Array.isArray(rows) ? rows : [rows];
191 }
192 /**
193 * Convert fetched results to an array of model instances
194 */
195 const modelInstances = rows.reduce((models, row) => {
196 if ((0, utils_2.isObject)(row)) {
197 const modelInstance = this.model.$createFromAdapterResult(row, this.sideloaded, this.clientOptions);
198 /**
199 * Transform row when row transformer is defined
200 */
201 if (this.rowTransformerCallback) {
202 this.rowTransformerCallback(modelInstance);
203 }
204 models.push(modelInstance);
205 }
206 return models;
207 }, []);
208 /**
209 * Preload for model instances
210 */
211 await this.preloader
212 .sideload(this.sideloaded)
213 .debug(this.debugQueries)
214 .processAllForMany(modelInstances, this.client);
215 return modelInstances;
216 }
217 /**
218 * Ensures that we are not executing `update` or `del` when using read only
219 * client
220 */
221 ensureCanPerformWrites() {
222 if (this.client && this.client.mode === 'read') {
223 throw new utils_1.Exception('Updates and deletes cannot be performed in read mode');
224 }
225 }
226 /**
227 * Defines sub query for checking the existance of a relationship
228 */
229 addWhereHas(relationName, boolean, operator, value, callback) {
230 let rawMethod = 'whereRaw';
231 let existsMethod = 'whereExists';
232 switch (boolean) {
233 case 'or':
234 rawMethod = 'orWhereRaw';
235 existsMethod = 'orWhereExists';
236 break;
237 case 'not':
238 existsMethod = 'whereNotExists';
239 break;
240 case 'orNot':
241 rawMethod = 'orWhereRaw';
242 existsMethod = 'orWhereNotExists';
243 break;
244 }
245 const subQuery = this.getRelationship(relationName).subQuery(this.client);
246 subQuery.selfJoinCounter = this.joinCounter;
247 /**
248 * Invoke callback when defined
249 */
250 if (typeof callback === 'function') {
251 callback(subQuery);
252 }
253 /**
254 * Count all when value and operator are defined.
255 */
256 if (value !== undefined && operator !== undefined) {
257 /**
258 * If user callback has not defined any aggregates, then we should
259 * add a count
260 */
261 if (!subQuery.hasAggregates) {
262 subQuery.count('*');
263 }
264 /**
265 * Pull sql and bindings from the query
266 */
267 const { sql, bindings } = subQuery.prepare().toSQL();
268 /**
269 * Define where raw clause. Query builder doesn't have any "whereNotRaw" method
270 * and hence we need to prepend the `NOT` keyword manually
271 */
272 boolean === 'orNot' || boolean === 'not'
273 ? this[rawMethod](`not (${sql}) ${operator} (?)`, bindings.concat([value]))
274 : this[rawMethod](`(${sql}) ${operator} (?)`, bindings.concat([value]));
275 return this;
276 }
277 /**
278 * Use where exists when no operator and value is defined
279 */
280 this[existsMethod](subQuery.prepare());
281 return this;
282 }
283 /**
284 * Returns the profiler action. Protected, since the class is extended
285 * by relationships
286 */
287 getQueryData() {
288 return {
289 connection: this.client.connectionName,
290 inTransaction: this.client.isTransaction,
291 model: this.model.name,
292 };
293 }
294 /**
295 * Returns the relationship instance from the model. An exception is
296 * raised when relationship is missing
297 */
298 getRelationship(name) {
299 const relation = this.model.$getRelation(name);
300 /**
301 * Ensure relationship exists
302 */
303 if (!relation) {
304 throw new utils_1.Exception(`"${name}" is not defined as a relationship on "${this.model.name}" model`, 500, 'E_UNDEFINED_RELATIONSHIP');
305 }
306 relation.boot();
307 return relation;
308 }
309 /**
310 * Define custom reporter data. It will be merged with
311 * the existing data
312 */
313 reporterData(data) {
314 this.customReporterData = data;
315 return this;
316 }
317 /**
318 * Define a custom callback to transform rows
319 */
320 rowTransformer(callback) {
321 this.rowTransformerCallback = callback;
322 return this;
323 }
324 /**
325 * Clone the current query builder
326 */
327 clone() {
328 const clonedQuery = new ModelQueryBuilder(this.knexQuery.clone(), this.model, this.client);
329 this.applyQueryFlags(clonedQuery);
330 clonedQuery.sideloaded = Object.assign({}, this.sideloaded);
331 clonedQuery.debug(this.debugQueries);
332 clonedQuery.reporterData(this.customReporterData);
333 this.rowTransformerCallback && this.rowTransformer(this.rowTransformerCallback);
334 return clonedQuery;
335 }
336 /**
337 * Define returning columns
338 */
339 returning(columns) {
340 /**
341 * Do not chain `returning` in sqlite3 to avoid knex warnings
342 */
343 if (this.client && ['sqlite3', 'mysql'].includes(this.client.dialect.name)) {
344 return this;
345 }
346 columns = Array.isArray(columns)
347 ? columns.map((column) => this.resolveKey(column))
348 : this.resolveKey(columns);
349 this.knexQuery.returning(columns);
350 return this;
351 }
352 /**
353 * Define a query to constraint to be defined when condition is truthy
354 */
355 ifDialect(dialects, matchCallback, noMatchCallback) {
356 dialects = Array.isArray(dialects) ? dialects : [dialects];
357 if (dialects.includes(this.client.dialect.name)) {
358 matchCallback(this);
359 }
360 else if (noMatchCallback) {
361 noMatchCallback(this);
362 }
363 return this;
364 }
365 /**
366 * Define a query to constraint to be defined when condition is falsy
367 */
368 unlessDialect(dialects, matchCallback, noMatchCallback) {
369 dialects = Array.isArray(dialects) ? dialects : [dialects];
370 if (!dialects.includes(this.client.dialect.name)) {
371 matchCallback(this);
372 }
373 else if (noMatchCallback) {
374 noMatchCallback(this);
375 }
376 return this;
377 }
378 /**
379 * Applies the query scopes on the current query builder
380 * instance
381 */
382 withScopes(callback) {
383 this.scopesWrapper = this.scopesWrapper || new ModelScopes(this);
384 callback(this.scopesWrapper);
385 return this;
386 }
387 /**
388 * Applies the query scopes on the current query builder
389 * instance
390 */
391 apply(callback) {
392 return this.withScopes(callback);
393 }
394 /**
395 * Define a custom preloader instance for preloading relationships
396 */
397 usePreloader(preloader) {
398 this.preloader = preloader;
399 return this;
400 }
401 /**
402 * Set sideloaded properties to be passed to the model instance
403 */
404 sideload(value) {
405 this.sideloaded = value;
406 return this;
407 }
408 /**
409 * Fetch and return first results from the results set. This method
410 * will implicitly set a `limit` on the query
411 */
412 async first() {
413 const isFetchCall = this.wrapResultsToModelInstances && this.knexQuery['_method'] === 'select';
414 if (isFetchCall) {
415 await this.model.$hooks.exec('before', 'find', this);
416 }
417 const result = await this.limit(1).execQuery();
418 if (result[0] && isFetchCall) {
419 await this.model.$hooks.exec('after', 'find', result[0]);
420 }
421 return result[0] || null;
422 }
423 /**
424 * Fetch and return first results from the results set. This method
425 * will implicitly set a `limit` on the query
426 */
427 async firstOrFail() {
428 const row = await this.first();
429 if (!row) {
430 throw new utils_1.Exception('Row not found', 404, 'E_ROW_NOT_FOUND');
431 }
432 return row;
433 }
434 /**
435 * Load aggregate value as a subquery for a relationship
436 */
437 withAggregate(relationName, userCallback) {
438 const subQuery = this.getRelationship(relationName).subQuery(this.client);
439 subQuery.selfJoinCounter = this.joinCounter;
440 /**
441 * Invoke user callback
442 */
443 userCallback(subQuery);
444 /**
445 * Raise exception if the callback has not defined an aggregate
446 */
447 if (!subQuery.hasAggregates) {
448 throw new utils_1.Exception('"withAggregate" callback must use an aggregate function');
449 }
450 /**
451 * Select "*" when no custom selects are defined
452 */
453 if (!this.columns.length) {
454 this.select(`${this.model.table}.*`);
455 }
456 /**
457 * Throw exception when no alias
458 */
459 if (!subQuery.subQueryAlias) {
460 throw new utils_1.Exception('"withAggregate" callback must define the alias for the aggregate query');
461 }
462 /**
463 * Count subquery selection
464 */
465 this.select(subQuery.prepare());
466 /**
467 * Bump the counter
468 */
469 this.joinCounter++;
470 return this;
471 }
472 /**
473 * Get count of a relationship along side the main query results
474 */
475 withCount(relationName, userCallback) {
476 this.withAggregate(relationName, (subQuery) => {
477 if (typeof userCallback === 'function') {
478 userCallback(subQuery);
479 }
480 /**
481 * Count "*"
482 */
483 if (!subQuery.hasAggregates) {
484 subQuery.count('*');
485 }
486 /**
487 * Define alias for the subquery
488 */
489 if (!subQuery.subQueryAlias) {
490 subQuery.as(`${relationName}_count`);
491 }
492 });
493 return this;
494 }
495 /**
496 * Add where constraint using the relationship
497 */
498 whereHas(relationName, callback, operator, value) {
499 return this.addWhereHas(relationName, 'and', operator, value, callback);
500 }
501 /**
502 * Add or where constraint using the relationship
503 */
504 orWhereHas(relationName, callback, operator, value) {
505 return this.addWhereHas(relationName, 'or', operator, value, callback);
506 }
507 /**
508 * Alias of [[whereHas]]
509 */
510 andWhereHas(relationName, callback, operator, value) {
511 return this.addWhereHas(relationName, 'and', operator, value, callback);
512 }
513 /**
514 * Add where not constraint using the relationship
515 */
516 whereDoesntHave(relationName, callback, operator, value) {
517 return this.addWhereHas(relationName, 'not', operator, value, callback);
518 }
519 /**
520 * Add or where not constraint using the relationship
521 */
522 orWhereDoesntHave(relationName, callback, operator, value) {
523 return this.addWhereHas(relationName, 'orNot', operator, value, callback);
524 }
525 /**
526 * Alias of [[whereDoesntHave]]
527 */
528 andWhereDoesntHave(relationName, callback, operator, value) {
529 return this.addWhereHas(relationName, 'not', operator, value, callback);
530 }
531 /**
532 * Add where constraint using the relationship
533 */
534 has(relationName, operator, value) {
535 return this.addWhereHas(relationName, 'and', operator, value);
536 }
537 /**
538 * Add or where constraint using the relationship
539 */
540 orHas(relationName, operator, value) {
541 return this.addWhereHas(relationName, 'or', operator, value);
542 }
543 /**
544 * Alias of [[has]]
545 */
546 andHas(relationName, operator, value) {
547 return this.addWhereHas(relationName, 'and', operator, value);
548 }
549 /**
550 * Add where not constraint using the relationship
551 */
552 doesntHave(relationName, operator, value) {
553 return this.addWhereHas(relationName, 'not', operator, value);
554 }
555 /**
556 * Add or where not constraint using the relationship
557 */
558 orDoesntHave(relationName, operator, value) {
559 return this.addWhereHas(relationName, 'orNot', operator, value);
560 }
561 /**
562 * Alias of [[doesntHave]]
563 */
564 andDoesntHave(relationName, operator, value) {
565 return this.addWhereHas(relationName, 'not', operator, value);
566 }
567 /**
568 * Define a relationship to be preloaded
569 */
570 preload(relationName, userCallback) {
571 this.preloader.load(relationName, userCallback);
572 return this;
573 }
574 /**
575 * Perform update by incrementing value for a given column. Increments
576 * can be clubbed with `update` as well
577 */
578 increment(column, counter) {
579 this.ensureCanPerformWrites();
580 this.knexQuery.increment(this.resolveKey(column, true), counter);
581 return this;
582 }
583 /**
584 * Perform update by decrementing value for a given column. Decrements
585 * can be clubbed with `update` as well
586 */
587 decrement(column, counter) {
588 this.ensureCanPerformWrites();
589 this.knexQuery.decrement(this.resolveKey(column, true), counter);
590 return this;
591 }
592 /**
593 * Perform update
594 */
595 update(column, value, returning) {
596 this.ensureCanPerformWrites();
597 if (value === undefined && returning === undefined) {
598 this.knexQuery.update(this.resolveKey(column, true));
599 }
600 else if (returning === undefined) {
601 this.knexQuery.update(this.resolveKey(column), value);
602 }
603 else {
604 this.knexQuery.update(this.resolveKey(column), value, returning);
605 }
606 return this;
607 }
608 /**
609 * Delete rows under the current query
610 */
611 del() {
612 this.ensureCanPerformWrites();
613 this.knexQuery.del();
614 return this;
615 }
616 /**
617 * Alias for [[del]]
618 */
619 delete() {
620 return this.del();
621 }
622 /**
623 * Turn on/off debugging for this query
624 */
625 debug(debug) {
626 this.debugQueries = debug;
627 return this;
628 }
629 /**
630 * Define query timeout
631 */
632 timeout(time, options) {
633 this.knexQuery['timeout'](time, options);
634 return this;
635 }
636 /**
637 * Returns SQL query as a string
638 */
639 toQuery() {
640 this.applyWhere();
641 return this.knexQuery.toQuery();
642 }
643 /**
644 * Run query inside the given transaction
645 */
646 useTransaction(transaction) {
647 this.knexQuery.transacting(transaction.knexClient);
648 return this;
649 }
650 /**
651 * Executes the query
652 */
653 async exec() {
654 const isFetchCall = this.wrapResultsToModelInstances && this.knexQuery['_method'] === 'select';
655 if (isFetchCall) {
656 await this.model.$hooks.exec('before', 'fetch', this);
657 }
658 const result = await this.execQuery();
659 if (isFetchCall) {
660 await this.model.$hooks.exec('after', 'fetch', result);
661 }
662 return result;
663 }
664 /**
665 * Paginate through rows inside a given table
666 */
667 async paginate(page, perPage = 20) {
668 const isFetchCall = this.wrapResultsToModelInstances && this.knexQuery['_method'] === 'select';
669 /**
670 * Cast to number
671 */
672 page = Number(page);
673 perPage = Number(perPage);
674 const countQuery = this.clone()
675 .clearOrder()
676 .clearLimit()
677 .clearOffset()
678 .clearSelect()
679 .count('* as total')
680 .pojo();
681 /**
682 * We pass both the counts query and the main query to the
683 * paginate hook
684 */
685 if (isFetchCall) {
686 await this.model.$hooks.exec('before', 'paginate', [countQuery, this]);
687 await this.model.$hooks.exec('before', 'fetch', this);
688 }
689 const aggregateResult = await countQuery.exec();
690 const total = this.hasGroupBy ? aggregateResult.length : aggregateResult[0].total;
691 const results = total > 0 ? await this.forPage(page, perPage).execQuery() : [];
692 /**
693 * Choose paginator
694 */
695 const paginator = this.wrapResultsToModelInstances
696 ? new Paginator_1.ModelPaginator(total, perPage, page, ...results)
697 : new SimplePaginator_1.SimplePaginator(total, perPage, page, ...results);
698 paginator.namingStrategy = this.model.namingStrategy;
699 if (isFetchCall) {
700 await this.model.$hooks.exec('after', 'paginate', paginator);
701 await this.model.$hooks.exec('after', 'fetch', results);
702 }
703 return paginator;
704 }
705 /**
706 * Get sql representation of the query
707 */
708 toSQL() {
709 this.applyWhere();
710 return this.knexQuery.toSQL();
711 }
712 /**
713 * Get rows back as a plain javascript object and not an array
714 * of model instances
715 */
716 pojo() {
717 this.wrapResultsToModelInstances = false;
718 return this;
719 }
720 /**
721 * Implementation of `then` for the promise API
722 */
723 then(resolve, reject) {
724 return this.exec().then(resolve, reject);
725 }
726 /**
727 * Implementation of `catch` for the promise API
728 */
729 catch(reject) {
730 return this.exec().catch(reject);
731 }
732 /**
733 * Implementation of `finally` for the promise API
734 */
735 finally(fullfilled) {
736 return this.exec().finally(fullfilled);
737 }
738 /**
739 * Required when Promises are extended
740 */
741 get [Symbol.toStringTag]() {
742 return this.constructor.name;
743 }
744}
745exports.ModelQueryBuilder = ModelQueryBuilder;
746/**
747 * Required by macroable
748 */
749Object.defineProperty(ModelQueryBuilder, "macros", {
750 enumerable: true,
751 configurable: true,
752 writable: true,
753 value: {}
754});
755Object.defineProperty(ModelQueryBuilder, "getters", {
756 enumerable: true,
757 configurable: true,
758 writable: true,
759 value: {}
760});