@villedemontreal/utils-knex 7.0.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 +236 -0
- package/dist/src/config/configs.d.ts +19 -0
- package/dist/src/config/configs.d.ts.map +1 -0
- package/dist/src/config/configs.js +27 -0
- package/dist/src/config/configs.js.map +1 -0
- package/dist/src/config/constants.d.ts +21 -0
- package/dist/src/config/constants.d.ts.map +1 -0
- package/dist/src/config/constants.js +21 -0
- package/dist/src/config/constants.js.map +1 -0
- package/dist/src/config/init.d.ts +16 -0
- package/dist/src/config/init.d.ts.map +1 -0
- package/dist/src/config/init.js +33 -0
- package/dist/src/config/init.js.map +1 -0
- package/dist/src/databaseContext.d.ts +9 -0
- package/dist/src/databaseContext.d.ts.map +1 -0
- package/dist/src/databaseContext.js +3 -0
- package/dist/src/databaseContext.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/knexUtils.d.ts +134 -0
- package/dist/src/knexUtils.d.ts.map +1 -0
- package/dist/src/knexUtils.js +349 -0
- package/dist/src/knexUtils.js.map +1 -0
- package/dist/src/knexUtils.test.d.ts +2 -0
- package/dist/src/knexUtils.test.d.ts.map +1 -0
- package/dist/src/knexUtils.test.js +897 -0
- package/dist/src/knexUtils.test.js.map +1 -0
- package/dist/src/transactionManager.d.ts +34 -0
- package/dist/src/transactionManager.d.ts.map +1 -0
- package/dist/src/transactionManager.js +75 -0
- package/dist/src/transactionManager.js.map +1 -0
- package/dist/src/transactionManager.test.d.ts +10 -0
- package/dist/src/transactionManager.test.d.ts.map +1 -0
- package/dist/src/transactionManager.test.js +255 -0
- package/dist/src/transactionManager.test.js.map +1 -0
- package/dist/src/utils/logger.d.ts +12 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +53 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/testingConfigurations.d.ts +9 -0
- package/dist/src/utils/testingConfigurations.d.ts.map +1 -0
- package/dist/src/utils/testingConfigurations.js +16 -0
- package/dist/src/utils/testingConfigurations.js.map +1 -0
- package/dist/testing/testClient.d.ts +15 -0
- package/dist/testing/testClient.d.ts.map +1 -0
- package/dist/testing/testClient.js +55 -0
- package/dist/testing/testClient.js.map +1 -0
- package/dist/testing/testRepo.d.ts +8 -0
- package/dist/testing/testRepo.d.ts.map +1 -0
- package/dist/testing/testRepo.js +31 -0
- package/dist/testing/testRepo.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +76 -0
- package/src/config/configs.ts +34 -0
- package/src/config/constants.ts +33 -0
- package/src/config/init.ts +33 -0
- package/src/databaseContext.ts +9 -0
- package/src/index.ts +9 -0
- package/src/knexUtils.test.ts +1526 -0
- package/src/knexUtils.ts +459 -0
- package/src/transactionManager.test.ts +302 -0
- package/src/transactionManager.ts +94 -0
- package/src/utils/logger.ts +60 -0
- package/src/utils/testingConfigurations.ts +13 -0
package/src/knexUtils.ts
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// ==========================================
|
|
2
|
+
// Knex utilities
|
|
3
|
+
//
|
|
4
|
+
// Ok in test files
|
|
5
|
+
// tslint:disable:no-string-literal
|
|
6
|
+
// ==========================================
|
|
7
|
+
import { IPaginatedResult, utils } from '@villedemontreal/general-utils';
|
|
8
|
+
import { Promise as BBPromise } from 'bluebird';
|
|
9
|
+
import knex, { Knex } from 'knex';
|
|
10
|
+
import * as _ from 'lodash';
|
|
11
|
+
import * as sinon from 'sinon';
|
|
12
|
+
import { v4 as uuid } from 'uuid';
|
|
13
|
+
import { createLogger } from './utils/logger';
|
|
14
|
+
|
|
15
|
+
const logger = createLogger('knexUtils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Knex utilities
|
|
19
|
+
*/
|
|
20
|
+
export class KnexUtils {
|
|
21
|
+
protected static KNEX_TOTAL_COUNT_QUERY_COLUMN_NAME = 'count';
|
|
22
|
+
protected static TOTAL_COUNT_BUILDER_OPTION_NAME = 'isTotalCountBuilder';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Takes a Knex.QueryBuilder, which is the object created
|
|
26
|
+
* when defining a Knex query, an returns the rows total count.
|
|
27
|
+
*
|
|
28
|
+
* This function is useful when you already have a SELECT Knex query,
|
|
29
|
+
* but you only need the rows total count instead of the rows themselves!
|
|
30
|
+
*
|
|
31
|
+
* Warning!! This function only works with SELECT queries, and
|
|
32
|
+
* may fail in some untested complex situations... It only has
|
|
33
|
+
* been tested with simple select/from/where/orderBy queries.
|
|
34
|
+
*
|
|
35
|
+
* Example :
|
|
36
|
+
*
|
|
37
|
+
* const queryBuilder = client
|
|
38
|
+
* .from(BOOKS_TABLE_NAME)
|
|
39
|
+
* .orderBy("author");
|
|
40
|
+
*
|
|
41
|
+
* ... and then, instead of executing the query, by using
|
|
42
|
+
* "then()" or "await", you pass the builder to the
|
|
43
|
+
* "totalCount()" function :
|
|
44
|
+
*
|
|
45
|
+
* let totalCount: number = await knexUtils.totalCount(client, queryBuilder);
|
|
46
|
+
*
|
|
47
|
+
*/
|
|
48
|
+
public async totalCount(knex: Knex, selectBuilder: Knex.QueryBuilder): Promise<number> {
|
|
49
|
+
const result = await this.paginateOrTotalCount(knex, selectBuilder, -1, -1, true);
|
|
50
|
+
return result.paging.totalCount;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Takes a Knex.QueryBuilder, which is the object created
|
|
55
|
+
* when defining a Knex query, a limit and a current
|
|
56
|
+
* page, then return a IPaginatedResult.
|
|
57
|
+
*
|
|
58
|
+
* In other words, instead of executing the query you
|
|
59
|
+
* are building with Knex directly, you pass the unsent
|
|
60
|
+
* builder to this function and you get :
|
|
61
|
+
* - Pagination for your query
|
|
62
|
+
* - The total number of elements your query would return if it
|
|
63
|
+
* wasn't paginated.
|
|
64
|
+
*
|
|
65
|
+
* Warning!! This function only works with SELECT queries, and
|
|
66
|
+
* may fail in some untested complex situations... It only has
|
|
67
|
+
* been tested with simple select/from/where/orderBy queries.
|
|
68
|
+
*
|
|
69
|
+
* If this function fails for one of your query, you'll have to
|
|
70
|
+
* duplicate the code of that query to make two separate queries :
|
|
71
|
+
* one for the rows only and one for the total count only.
|
|
72
|
+
*
|
|
73
|
+
* For example, to get 3 items starting at offset 9 :
|
|
74
|
+
*
|
|
75
|
+
* const paginatedResult = await knexUtils.paginate(
|
|
76
|
+
* client,
|
|
77
|
+
* client
|
|
78
|
+
* .select("id", "author", "title")
|
|
79
|
+
* .from(BOOKS_TABLE_NAME)
|
|
80
|
+
* .orderBy("author"),
|
|
81
|
+
* 9, 3);
|
|
82
|
+
*/
|
|
83
|
+
public async paginate(
|
|
84
|
+
knex: Knex,
|
|
85
|
+
selectBuilder: Knex.QueryBuilder,
|
|
86
|
+
offset: number,
|
|
87
|
+
limit: number,
|
|
88
|
+
): Promise<IPaginatedResult<any>> {
|
|
89
|
+
const result = await this.paginateOrTotalCount(knex, selectBuilder, offset, limit);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a mocked Knex client, linked to a dummy database.
|
|
95
|
+
* The client allows you to define stubs that will simulate
|
|
96
|
+
* the result from the DB.
|
|
97
|
+
*
|
|
98
|
+
* Useful for testing!
|
|
99
|
+
*/
|
|
100
|
+
public createKnexMockedClient = async (): Promise<IKnexMockedClient> => {
|
|
101
|
+
const knexMockedClient: IKnexMockedClient = knex({
|
|
102
|
+
client: 'sqlite',
|
|
103
|
+
connection: {
|
|
104
|
+
filename: './mydb.sqlite',
|
|
105
|
+
},
|
|
106
|
+
useNullAsDefault: true,
|
|
107
|
+
}) as any;
|
|
108
|
+
|
|
109
|
+
// ==========================================
|
|
110
|
+
// We add stubs that will allow to change the
|
|
111
|
+
// mocked result of a query.
|
|
112
|
+
// ==========================================
|
|
113
|
+
knexMockedClient.resultStub = sinon.stub();
|
|
114
|
+
knexMockedClient.resultStub.returns([]);
|
|
115
|
+
|
|
116
|
+
knexMockedClient.totalCountStub = sinon.stub();
|
|
117
|
+
knexMockedClient.totalCountStub.returns(0);
|
|
118
|
+
|
|
119
|
+
knexMockedClient.beforeQuerySpy = sinon.spy();
|
|
120
|
+
|
|
121
|
+
// ==========================================
|
|
122
|
+
// Returns a dummy connection object
|
|
123
|
+
// ==========================================
|
|
124
|
+
knexMockedClient.client.acquireConnection = () => {
|
|
125
|
+
// ==========================================
|
|
126
|
+
// We have to use a BlueBird Promise because
|
|
127
|
+
// this is what Knex is expecting and some functions
|
|
128
|
+
// specific to BlueBird promises are called.
|
|
129
|
+
// ==========================================
|
|
130
|
+
const promiseLike = new BBPromise((resolve: any, reject: any) => {
|
|
131
|
+
resolve({
|
|
132
|
+
// The "__knexUid" property is required by Knex
|
|
133
|
+
__knexUid: uuid(),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
return promiseLike;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ==========================================
|
|
140
|
+
// Called when the query is actually executed
|
|
141
|
+
// by Knex. We simply return the values from
|
|
142
|
+
// our stubs!
|
|
143
|
+
// ==========================================
|
|
144
|
+
knexMockedClient.client.query = (knexUuid: string, builder: any) => {
|
|
145
|
+
// The spy...
|
|
146
|
+
knexMockedClient.beforeQuerySpy(builder);
|
|
147
|
+
|
|
148
|
+
// ==========================================
|
|
149
|
+
// Query for the "totalCount" part of our "paginate()"
|
|
150
|
+
// function... We return the value from the
|
|
151
|
+
// *totalCount stub*.
|
|
152
|
+
// ==========================================
|
|
153
|
+
if (builder.options && builder.options[KnexUtils.TOTAL_COUNT_BUILDER_OPTION_NAME]) {
|
|
154
|
+
const theResult = knexMockedClient.totalCountStub();
|
|
155
|
+
const resultRow = {};
|
|
156
|
+
(resultRow as any)[KnexUtils.KNEX_TOTAL_COUNT_QUERY_COLUMN_NAME] = theResult;
|
|
157
|
+
return Promise.resolve(resultRow);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ==========================================
|
|
161
|
+
// Regular query
|
|
162
|
+
// ==========================================
|
|
163
|
+
|
|
164
|
+
// The stub....
|
|
165
|
+
const result = knexMockedClient.resultStub();
|
|
166
|
+
return Promise.resolve(result);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ==========================================
|
|
170
|
+
// Called by Knex to transform the result returned
|
|
171
|
+
// by the DB. We return our mocked result as is...
|
|
172
|
+
// ==========================================
|
|
173
|
+
knexMockedClient.client.processResponse = (obj: any, runner: any) => {
|
|
174
|
+
return obj;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ==========================================
|
|
178
|
+
// When using a transaction, a new "client"
|
|
179
|
+
// is created. We replace it by our mocked client.
|
|
180
|
+
// ==========================================
|
|
181
|
+
(knexMockedClient as any)['context'].transaction = (transactionScope?: null, config?: any) => {
|
|
182
|
+
return transactionScope !== null ? (transactionScope as any)(knexMockedClient) : undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return knexMockedClient;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* For Oracle.
|
|
190
|
+
* Wraps a column name (or a "?") with LOWER or with a CONVERT function
|
|
191
|
+
* which will strip accents.
|
|
192
|
+
*/
|
|
193
|
+
public wrapWithOracleModificationkeywords(
|
|
194
|
+
columnNameOrInterrogationMark: string,
|
|
195
|
+
isConvert: boolean,
|
|
196
|
+
isLower: boolean,
|
|
197
|
+
): string {
|
|
198
|
+
if (isConvert && isLower) {
|
|
199
|
+
return `LOWER(CONVERT(${columnNameOrInterrogationMark}, 'US7ASCII', 'WE8ISO8859P1'))`;
|
|
200
|
+
}
|
|
201
|
+
if (isConvert) {
|
|
202
|
+
return `CONVERT(${columnNameOrInterrogationMark}, 'US7ASCII', 'WE8ISO8859P1')`;
|
|
203
|
+
}
|
|
204
|
+
if (isLower) {
|
|
205
|
+
return `LOWER(${columnNameOrInterrogationMark})`;
|
|
206
|
+
}
|
|
207
|
+
return columnNameOrInterrogationMark;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* For Oracle.
|
|
212
|
+
* Adds a LIKE clause, where the values can be compared lowercased (isLower), by removing the
|
|
213
|
+
* accents first (isConvert), and where the "val" can starts or ends with a "*" wildcard.
|
|
214
|
+
*/
|
|
215
|
+
public addOracleLikeClause(
|
|
216
|
+
queryBuilder: Knex.QueryBuilder,
|
|
217
|
+
columnName: string,
|
|
218
|
+
val: string,
|
|
219
|
+
isConvert: boolean,
|
|
220
|
+
isLower: boolean,
|
|
221
|
+
): Knex.QueryBuilder {
|
|
222
|
+
let valClean = val;
|
|
223
|
+
let wildcardPrefix = '';
|
|
224
|
+
let wildcardSuffix = '';
|
|
225
|
+
if (val.startsWith('*')) {
|
|
226
|
+
wildcardPrefix = "'%' || ";
|
|
227
|
+
valClean = _.trimStart(valClean, '*');
|
|
228
|
+
}
|
|
229
|
+
if (valClean.endsWith('*')) {
|
|
230
|
+
wildcardSuffix = " || '%'";
|
|
231
|
+
valClean = _.trimEnd(valClean, '*');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let queryBuilderClean = queryBuilder;
|
|
235
|
+
if (wildcardPrefix === '' && wildcardSuffix === '' && !isConvert && !isLower) {
|
|
236
|
+
queryBuilderClean = queryBuilderClean.where(columnName, valClean);
|
|
237
|
+
} else {
|
|
238
|
+
const clause = `${this.wrapWithOracleModificationkeywords(
|
|
239
|
+
columnName,
|
|
240
|
+
isConvert,
|
|
241
|
+
isLower,
|
|
242
|
+
)} LIKE ${wildcardPrefix}${this.wrapWithOracleModificationkeywords(
|
|
243
|
+
'?',
|
|
244
|
+
isConvert,
|
|
245
|
+
isLower,
|
|
246
|
+
)}${wildcardSuffix}`;
|
|
247
|
+
queryBuilderClean = queryBuilderClean.whereRaw(clause, valClean);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return queryBuilderClean;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* For SQL Server.
|
|
255
|
+
* Wraps a column name (or a "?") with LOWER and/or with a CAST function
|
|
256
|
+
* which will strip accents.
|
|
257
|
+
*/
|
|
258
|
+
public wrapWithSqlServerModificationKeywords(
|
|
259
|
+
columnNameOrInterrogationMark: string,
|
|
260
|
+
isConvert: boolean,
|
|
261
|
+
isLower: boolean,
|
|
262
|
+
): string {
|
|
263
|
+
if (!isLower && !isConvert) {
|
|
264
|
+
return columnNameOrInterrogationMark;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isLower && !isConvert) {
|
|
268
|
+
return `LOWER(${columnNameOrInterrogationMark})`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ==========================================
|
|
272
|
+
// There is no magic method in SQL Server to strip accents.
|
|
273
|
+
// This is the best I found : https://stackoverflow.com/a/3578644/843699
|
|
274
|
+
// ... I added "œ" and "æ" management too, which don't work with the
|
|
275
|
+
// Stack Overflow trick.
|
|
276
|
+
// ==========================================
|
|
277
|
+
const cast =
|
|
278
|
+
`CAST(` +
|
|
279
|
+
`REPLACE(REPLACE(REPLACE(REPLACE(${columnNameOrInterrogationMark}, 'œ', 'oe'), 'Œ', 'OE'), 'æ', 'ae'), 'Æ', 'AE')` +
|
|
280
|
+
`AS VARCHAR(max)) COLLATE SQL_Latin1_General_Cp1251_CS_AS`;
|
|
281
|
+
|
|
282
|
+
if (!isLower) {
|
|
283
|
+
return cast;
|
|
284
|
+
}
|
|
285
|
+
return `LOWER(${cast})`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* For SQL Server.
|
|
290
|
+
* Adds a LIKE clause, where the values can be compared lowercased (lower), by removing the
|
|
291
|
+
* accents first (removeAccent), and where the "val" can starts or ends with a "*" wildcard
|
|
292
|
+
* (acceptWildcard).
|
|
293
|
+
*/
|
|
294
|
+
public addSqlServerLikeClause(
|
|
295
|
+
queryBuilder: Knex.QueryBuilder,
|
|
296
|
+
columnName: string,
|
|
297
|
+
val: string,
|
|
298
|
+
acceptWildcard: boolean,
|
|
299
|
+
removeAccents: boolean,
|
|
300
|
+
lower: boolean,
|
|
301
|
+
): Knex.QueryBuilder {
|
|
302
|
+
let valClean = val;
|
|
303
|
+
let wildcardPrefix = '';
|
|
304
|
+
let wildcardSuffix = '';
|
|
305
|
+
|
|
306
|
+
if (acceptWildcard) {
|
|
307
|
+
if (val.startsWith('*')) {
|
|
308
|
+
wildcardPrefix = "'%' + ";
|
|
309
|
+
valClean = _.trimStart(valClean, '*');
|
|
310
|
+
}
|
|
311
|
+
if (valClean.endsWith('*')) {
|
|
312
|
+
wildcardSuffix = " + '%'";
|
|
313
|
+
valClean = _.trimEnd(valClean, '*');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let queryBuilderClean = queryBuilder;
|
|
318
|
+
if (wildcardPrefix === '' && wildcardSuffix === '' && !removeAccents && !lower) {
|
|
319
|
+
queryBuilderClean = queryBuilderClean.where(columnName, valClean);
|
|
320
|
+
} else {
|
|
321
|
+
const clause = `${this.wrapWithSqlServerModificationKeywords(
|
|
322
|
+
columnName,
|
|
323
|
+
removeAccents,
|
|
324
|
+
lower,
|
|
325
|
+
)} LIKE ${wildcardPrefix}${this.wrapWithSqlServerModificationKeywords(
|
|
326
|
+
'?',
|
|
327
|
+
removeAccents,
|
|
328
|
+
lower,
|
|
329
|
+
)}${wildcardSuffix}`;
|
|
330
|
+
queryBuilderClean = queryBuilderClean.whereRaw(clause, valClean);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return queryBuilderClean;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param totalCountOnly if true, only the request to get the total count will
|
|
338
|
+
* be made and an empty array will be returned as the rows.
|
|
339
|
+
*/
|
|
340
|
+
protected async paginateOrTotalCount(
|
|
341
|
+
knex: Knex,
|
|
342
|
+
selectBuilder: Knex.QueryBuilder,
|
|
343
|
+
offset: number,
|
|
344
|
+
limit: number,
|
|
345
|
+
totalCountOnly = false,
|
|
346
|
+
): Promise<IPaginatedResult<any>> {
|
|
347
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
348
|
+
if (!selectBuilder) {
|
|
349
|
+
throw new Error('A Knex SELECT query builder is required.');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if ((selectBuilder as any)['_method'] !== 'select') {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"The 'paginate()' and 'totalCount()' functions are only available on a SELECT query builder!",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let offsetClean = offset;
|
|
359
|
+
if (!utils.isIntegerValue(offsetClean, true, true)) {
|
|
360
|
+
logger.debug(`Invalid offset "${offsetClean}", 0 will be used instead`);
|
|
361
|
+
offsetClean = 0;
|
|
362
|
+
}
|
|
363
|
+
offsetClean = Number(offsetClean);
|
|
364
|
+
let limitClean = limit;
|
|
365
|
+
if (!utils.isIntegerValue(limitClean, true, false)) {
|
|
366
|
+
logger.debug(`Invalid limit "${limitClean}", 1 will be used instead`);
|
|
367
|
+
limitClean = 1;
|
|
368
|
+
}
|
|
369
|
+
limitClean = Number(limitClean);
|
|
370
|
+
|
|
371
|
+
// ==========================================
|
|
372
|
+
// We wrap the original select query in a
|
|
373
|
+
// "count()" query. We also add an option to this
|
|
374
|
+
// new query so we can later know this is the "totalCount"
|
|
375
|
+
// generated query.
|
|
376
|
+
// ==========================================
|
|
377
|
+
let countBuilder = knex.count(`* AS ${KnexUtils.KNEX_TOTAL_COUNT_QUERY_COLUMN_NAME}`);
|
|
378
|
+
const options: any = {};
|
|
379
|
+
options[KnexUtils.TOTAL_COUNT_BUILDER_OPTION_NAME] = true;
|
|
380
|
+
countBuilder = countBuilder.options(options);
|
|
381
|
+
|
|
382
|
+
// ==========================================
|
|
383
|
+
// For the subquery, we clone the
|
|
384
|
+
// original query but remove the "order by"
|
|
385
|
+
// clause, the limit and the offset.
|
|
386
|
+
// ==========================================
|
|
387
|
+
const countBuilderSubSelect: Knex.QueryBuilder = selectBuilder.clone();
|
|
388
|
+
delete (countBuilderSubSelect as any)['_single'].offset;
|
|
389
|
+
delete (countBuilderSubSelect as any)['_single'].limit;
|
|
390
|
+
if ((countBuilderSubSelect as any)['_statements']) {
|
|
391
|
+
const totalCountStatements = [];
|
|
392
|
+
for (const statement of (countBuilderSubSelect as any)['_statements']) {
|
|
393
|
+
if (statement.grouping !== 'order') {
|
|
394
|
+
totalCountStatements.push(statement);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
(countBuilderSubSelect as any)['_statements'] = totalCountStatements;
|
|
398
|
+
}
|
|
399
|
+
countBuilder = countBuilder.from(countBuilderSubSelect.as('_knexSub') as any);
|
|
400
|
+
|
|
401
|
+
// ==========================================
|
|
402
|
+
// Are we simply interested in the total count,
|
|
403
|
+
// or the actual rows too?
|
|
404
|
+
// We do not use something like Promise.all() to
|
|
405
|
+
// run both queries because of :
|
|
406
|
+
// https://github.com/tediousjs/node-mssql/issues/491
|
|
407
|
+
// ==========================================
|
|
408
|
+
const rs = await countBuilder.first();
|
|
409
|
+
const totalCount = Number(rs[KnexUtils.KNEX_TOTAL_COUNT_QUERY_COLUMN_NAME]);
|
|
410
|
+
|
|
411
|
+
let rows: any[];
|
|
412
|
+
if (!totalCountOnly) {
|
|
413
|
+
rows = await selectBuilder.offset(offsetClean).limit(limitClean);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const result: IPaginatedResult<any> = {
|
|
417
|
+
items: rows,
|
|
418
|
+
paging: {
|
|
419
|
+
totalCount,
|
|
420
|
+
limit: limitClean,
|
|
421
|
+
offset: offsetClean,
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export const knexUtils: KnexUtils = new KnexUtils();
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A Mocked Knex client.
|
|
432
|
+
*/
|
|
433
|
+
export interface IKnexMockedClient extends Knex {
|
|
434
|
+
/**
|
|
435
|
+
* The stub that is going to return the result
|
|
436
|
+
* when the query is executed.
|
|
437
|
+
*
|
|
438
|
+
* Defaults to an empty array.
|
|
439
|
+
*/
|
|
440
|
+
resultStub: sinon.SinonStub;
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* If the query is paginated, this stub
|
|
444
|
+
* will return the "totalCount" part of
|
|
445
|
+
* the result.
|
|
446
|
+
*
|
|
447
|
+
* Defaults to 0.
|
|
448
|
+
*/
|
|
449
|
+
totalCountStub: sinon.SinonStub;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* This spy is going to be called just before
|
|
453
|
+
* Knex actually execute the query. The builder
|
|
454
|
+
* has at this point been converted to a
|
|
455
|
+
* SQL string and you have access to this SQL and
|
|
456
|
+
* other informations.
|
|
457
|
+
*/
|
|
458
|
+
beforeQuerySpy: sinon.SinonSpy;
|
|
459
|
+
}
|