UNPKG

7.35 kBJavaScriptView Raw
1/* eslint no-console: 0 */
2
3// Helpers
4// ---------------
5
6const _ = require('lodash');
7const Promise = require('bluebird');
8const Model = require('./base/model');
9
10function ensureIntWithDefault(number, defaultValue) {
11 if (!number) return defaultValue;
12 const parsedNumber = parseInt(number, 10);
13 if (Number.isNaN(parsedNumber)) return defaultValue;
14
15 return parsedNumber;
16}
17
18module.exports = {
19 // This is used by both Model and Collection methods to paginate the results.
20 fetchPage(options) {
21 const DEFAULT_LIMIT = 10;
22 const DEFAULT_OFFSET = 0;
23 const DEFAULT_PAGE = 1;
24
25 const isModel = this instanceof Model;
26 const fetchOptions = _.omit(options, ['page', 'pageSize', 'limit', 'offset']);
27 const countOptions = _.omit(fetchOptions, ['require', 'columns', 'withRelated', 'lock']);
28 const fetchMethodName = isModel ? 'fetchAll' : 'fetch';
29 const targetModel = isModel ? this.constructor : this.target || this.model;
30 const tableName = targetModel.prototype.tableName;
31 const idAttribute = targetModel.prototype.idAttribute || 'id';
32 const targetIdColumn = [`${tableName}.${idAttribute}`];
33 let page;
34 let pageSize;
35 let limit;
36 let offset;
37
38 if (!options.limit && !options.offset) {
39 pageSize = ensureIntWithDefault(options.pageSize, DEFAULT_LIMIT);
40 page = ensureIntWithDefault(options.page, DEFAULT_PAGE);
41 limit = pageSize;
42 offset = limit * (page - 1);
43 } else {
44 limit = ensureIntWithDefault(options.limit, DEFAULT_LIMIT);
45 offset = ensureIntWithDefault(options.offset, DEFAULT_OFFSET);
46 }
47
48 const paginate = () => {
49 return this.clone()
50 .query((qb) => {
51 Object.assign(qb, this.query().clone());
52 qb.limit.apply(qb, [limit]);
53 qb.offset.apply(qb, [offset]);
54
55 return null;
56 })
57 [fetchMethodName](fetchOptions);
58 };
59
60 const count = () => {
61 const notNeededQueries = ['orderByBasic', 'orderByRaw', 'groupByBasic', 'groupByRaw'];
62 const counter = this.clone();
63 const groupColumns = [];
64
65 return counter
66 .query((qb) => {
67 Object.assign(qb, this.query().clone());
68
69 // Remove grouping and ordering. Ordering is unnecessary for a count, and grouping returns the entire result
70 // set. What we want instead is to use `DISTINCT`.
71 _.remove(qb._statements, (statement) => {
72 if (statement.grouping === 'group') statement.value.forEach((value) => groupColumns.push(value));
73 if (statement.grouping === 'columns' && statement.distinct)
74 statement.value.forEach((value) => groupColumns.push(value));
75
76 return notNeededQueries.indexOf(statement.type) > -1 || statement.grouping === 'columns';
77 });
78
79 if (!isModel && counter.relatedData) {
80 // Remove joining columns that break COUNT operation, eg. pivotal coulmns for belongsToMany relation.
81 counter.relatedData.joinColumns = function() {};
82 }
83
84 qb.countDistinct.apply(qb, groupColumns.length > 0 ? groupColumns : targetIdColumn);
85 })
86 [fetchMethodName](countOptions)
87 .then((result) => {
88 const metadata = !options.limit && !options.offset ? {page, pageSize} : {offset, limit};
89
90 if (result && result.length == 1) {
91 // We shouldn't have to do this, instead it should be result.models[0].get('count') but SQLite and MySQL
92 // return a really strange key name and Knex doesn't abstract that away yet:
93 // https://github.com/tgriesser/knex/issues/3315.
94 const keys = Object.keys(result.models[0].attributes);
95
96 if (keys.length === 1) {
97 const key = Object.keys(result.models[0].attributes)[0];
98 metadata.rowCount = parseInt(result.models[0].attributes[key]);
99 }
100 }
101
102 return metadata;
103 });
104 };
105
106 return Promise.join(paginate(), count(), (rows, metadata) => {
107 const pageCount = Math.ceil(metadata.rowCount / limit);
108 const pageData = Object.assign(metadata, {pageCount});
109 return Object.assign(rows, {pagination: pageData});
110 });
111 },
112
113 // Sets the constraints necessary during a `model.save` call.
114 saveConstraints: function(model, relatedData) {
115 const data = {};
116
117 if (
118 relatedData &&
119 !relatedData.isThrough() &&
120 relatedData.type !== 'belongsToMany' &&
121 relatedData.type !== 'belongsTo'
122 ) {
123 data[relatedData.key('foreignKey')] = relatedData.parentFk || model.get(relatedData.key('foreignKey'));
124 if (relatedData.isMorph()) data[relatedData.key('morphKey')] = relatedData.key('morphValue');
125 }
126
127 return model.set(model.parse(data));
128 },
129
130 // Finds the specific `morphTo` target Model we should be working with, or throws
131 // an error if none is matched.
132 morphCandidate: function(candidates, morphValue) {
133 const Target = _.find(candidates, (candidate) => candidate[1] === morphValue);
134
135 if (!Target)
136 throw new Error('The target polymorphic type "' + morphValue + '" is not one of the defined target types');
137
138 return Target[0];
139 },
140
141 // If there are no arguments, return the current object's
142 // query builder (or create and return a new one). If there are arguments,
143 // call the query builder with the first argument, applying the rest.
144 // If the first argument is an object, assume the keys are query builder
145 // methods, and the values are the arguments for the query.
146 query: function(obj, args) {
147 // Ensure the object has a query builder.
148 if (!obj._knex) {
149 const tableName = _.result(obj, 'tableName');
150 obj._knex = obj._builder(tableName);
151 }
152
153 // If there are no arguments, return the query builder.
154 if (args.length === 0) return obj._knex;
155
156 const method = args[0];
157
158 if (_.isFunction(method)) {
159 // `method` is a query builder callback. Call it on the query builder object.
160 method.call(obj._knex, obj._knex);
161 } else if (_.isObject(method)) {
162 // `method` is an object. Use keys as methods and values as arguments to
163 // the query builder.
164 for (const key in method) {
165 const target = Array.isArray(method[key]) ? method[key] : [method[key]];
166 obj._knex[key].apply(obj._knex, target);
167 }
168 } else {
169 // Otherwise assume that the `method` is string name of a query builder
170 // method, and use the remaining args as arguments to that method.
171 obj._knex[method].apply(obj._knex, args.slice(1));
172 }
173
174 return obj;
175 },
176
177 orderBy: function(obj, sort, order) {
178 let tableName;
179 let idAttribute;
180 let _sort;
181
182 if (obj.model) {
183 tableName = obj.model.prototype.tableName;
184 idAttribute = obj.model.prototype.idAttribute || 'id';
185 } else {
186 tableName = obj.constructor.prototype.tableName;
187 idAttribute = obj.constructor.prototype.idAttribute || 'id';
188 }
189
190 if (sort && sort.indexOf('-') === 0) {
191 _sort = sort.slice(1);
192 } else if (sort) {
193 _sort = sort;
194 } else {
195 _sort = idAttribute;
196 }
197
198 const _order = order || (sort && sort.indexOf('-') === 0 ? 'DESC' : 'ASC');
199
200 if (_sort.indexOf('.') === -1) {
201 _sort = `${tableName}.${_sort}`;
202 }
203
204 return obj.query((qb) => {
205 qb.orderBy(_sort, _order);
206 });
207 }
208};