@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.
Files changed (66) hide show
  1. package/README.md +236 -0
  2. package/dist/src/config/configs.d.ts +19 -0
  3. package/dist/src/config/configs.d.ts.map +1 -0
  4. package/dist/src/config/configs.js +27 -0
  5. package/dist/src/config/configs.js.map +1 -0
  6. package/dist/src/config/constants.d.ts +21 -0
  7. package/dist/src/config/constants.d.ts.map +1 -0
  8. package/dist/src/config/constants.js +21 -0
  9. package/dist/src/config/constants.js.map +1 -0
  10. package/dist/src/config/init.d.ts +16 -0
  11. package/dist/src/config/init.d.ts.map +1 -0
  12. package/dist/src/config/init.js +33 -0
  13. package/dist/src/config/init.js.map +1 -0
  14. package/dist/src/databaseContext.d.ts +9 -0
  15. package/dist/src/databaseContext.d.ts.map +1 -0
  16. package/dist/src/databaseContext.js +3 -0
  17. package/dist/src/databaseContext.js.map +1 -0
  18. package/dist/src/index.d.ts +5 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/index.js +26 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/knexUtils.d.ts +134 -0
  23. package/dist/src/knexUtils.d.ts.map +1 -0
  24. package/dist/src/knexUtils.js +349 -0
  25. package/dist/src/knexUtils.js.map +1 -0
  26. package/dist/src/knexUtils.test.d.ts +2 -0
  27. package/dist/src/knexUtils.test.d.ts.map +1 -0
  28. package/dist/src/knexUtils.test.js +897 -0
  29. package/dist/src/knexUtils.test.js.map +1 -0
  30. package/dist/src/transactionManager.d.ts +34 -0
  31. package/dist/src/transactionManager.d.ts.map +1 -0
  32. package/dist/src/transactionManager.js +75 -0
  33. package/dist/src/transactionManager.js.map +1 -0
  34. package/dist/src/transactionManager.test.d.ts +10 -0
  35. package/dist/src/transactionManager.test.d.ts.map +1 -0
  36. package/dist/src/transactionManager.test.js +255 -0
  37. package/dist/src/transactionManager.test.js.map +1 -0
  38. package/dist/src/utils/logger.d.ts +12 -0
  39. package/dist/src/utils/logger.d.ts.map +1 -0
  40. package/dist/src/utils/logger.js +53 -0
  41. package/dist/src/utils/logger.js.map +1 -0
  42. package/dist/src/utils/testingConfigurations.d.ts +9 -0
  43. package/dist/src/utils/testingConfigurations.d.ts.map +1 -0
  44. package/dist/src/utils/testingConfigurations.js +16 -0
  45. package/dist/src/utils/testingConfigurations.js.map +1 -0
  46. package/dist/testing/testClient.d.ts +15 -0
  47. package/dist/testing/testClient.d.ts.map +1 -0
  48. package/dist/testing/testClient.js +55 -0
  49. package/dist/testing/testClient.js.map +1 -0
  50. package/dist/testing/testRepo.d.ts +8 -0
  51. package/dist/testing/testRepo.d.ts.map +1 -0
  52. package/dist/testing/testRepo.js +31 -0
  53. package/dist/testing/testRepo.js.map +1 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -0
  55. package/package.json +76 -0
  56. package/src/config/configs.ts +34 -0
  57. package/src/config/constants.ts +33 -0
  58. package/src/config/init.ts +33 -0
  59. package/src/databaseContext.ts +9 -0
  60. package/src/index.ts +9 -0
  61. package/src/knexUtils.test.ts +1526 -0
  62. package/src/knexUtils.ts +459 -0
  63. package/src/transactionManager.test.ts +302 -0
  64. package/src/transactionManager.ts +94 -0
  65. package/src/utils/logger.ts +60 -0
  66. package/src/utils/testingConfigurations.ts +13 -0
@@ -0,0 +1,302 @@
1
+ // ==========================================
2
+ // Disabling some linting rules is OK in test files.
3
+ // tslint:disable:max-func-body-length
4
+ // tslint:disable:cyclomatic-complexity
5
+ // tslint:disable:no-string-literal
6
+ // ==========================================
7
+ import { assert } from 'chai';
8
+ import { Knex } from 'knex';
9
+ import { v4 as uuid } from 'uuid';
10
+ import { destroyTestKnexClient, getTestTransactionManager } from '../testing/testClient';
11
+ import { testRepo } from '../testing/testRepo';
12
+ import { IDatabaseContext } from './databaseContext';
13
+ import { KnexTransactionManager } from './transactionManager';
14
+
15
+ export interface ITestContext extends IDatabaseContext {
16
+ executionId: string;
17
+ }
18
+
19
+ export interface ITestUser {
20
+ id: number;
21
+ firstName: string;
22
+ lastName: string;
23
+ }
24
+
25
+ async function createUsersTable(knexClient: Knex) {
26
+ await knexClient.schema.createTable(`users`, async (table) => {
27
+ table.increments('id').primary();
28
+ table.string('firstName', 256).notNullable();
29
+ table.string('lastName', 256).notNullable();
30
+ });
31
+ }
32
+
33
+ describe(`Transactions Manager tests`, () => {
34
+ let testContext: ITestContext;
35
+
36
+ before(async () => {
37
+ // ==========================================
38
+ // We create an initial "context". In an API
39
+ // project, this would probably be done in
40
+ // a controller. The controller would then
41
+ // pass this context to all methods called
42
+ // on services and, indirectly, on repositories.
43
+ // ==========================================
44
+ testContext = {
45
+ currentKnexClient: null,
46
+ executionId: uuid(),
47
+ } as ITestContext;
48
+
49
+ // ==========================================
50
+ // Create the "users" table
51
+ // ==========================================
52
+ await withTransaction(testContext, async (client: Knex.Transaction) => {
53
+ await createUsersTable(client);
54
+ });
55
+ });
56
+
57
+ after(async () => {
58
+ await destroyTestKnexClient();
59
+ });
60
+
61
+ async function withClient<T>(context: IDatabaseContext, fnt: (client: Knex) => Promise<T>) {
62
+ const txManager: KnexTransactionManager = await getTestTransactionManager();
63
+ return txManager.withClient<T>(context, fnt);
64
+ }
65
+
66
+ async function withTransaction<T>(context: IDatabaseContext, fnt: (client: Knex) => Promise<T>) {
67
+ const txManager: KnexTransactionManager = await getTestTransactionManager();
68
+ return txManager.withTransaction<T>(context, fnt);
69
+ }
70
+
71
+ async function getUserById(id: number): Promise<ITestUser> {
72
+ return await withClient(testContext, async (client: Knex.Transaction) => {
73
+ const res = await client.select('id', 'firstName', 'lastName').from(`users`).where(`id`, id);
74
+
75
+ return {
76
+ id: res[0].id,
77
+ firstName: res[0].firstName,
78
+ lastName: res[0].lastName,
79
+ };
80
+ });
81
+ }
82
+
83
+ let userId: number;
84
+ it(`insert a user`, async () => {
85
+ // ==========================================
86
+ // Here we use "withClient" not "withTransaction"
87
+ // since we do not need a transaction. We
88
+ // *could* start a transaction even if it's
89
+ // not required, it would still work, but it
90
+ // is more costy...
91
+ // ==========================================
92
+ await withClient(testContext, async (client: Knex.Transaction) => {
93
+ const res = await client(`users`).insert({
94
+ firstName: 'Stromgol',
95
+ lastName: 'LaRoche',
96
+ } as ITestUser);
97
+ userId = res[0];
98
+ });
99
+
100
+ const user: ITestUser = await getUserById(userId);
101
+ assert.isOk(user);
102
+ assert.deepEqual(user.id, userId);
103
+ assert.deepEqual(user.firstName, 'Stromgol');
104
+ assert.deepEqual(user.lastName, 'LaRoche');
105
+ });
106
+
107
+ it(`Modifiy the lastName - not in a trasaction - succes`, async () => {
108
+ await withClient(testContext, async (client: Knex.Transaction) => {
109
+ await client(`users`)
110
+ .update({
111
+ lastName: 'LaPierre',
112
+ })
113
+ .where(`id`, userId);
114
+
115
+ await client(`users`)
116
+ .update({
117
+ lastName: 'Stone',
118
+ })
119
+ .where(`id`, userId);
120
+ });
121
+
122
+ const user: ITestUser = await getUserById(userId);
123
+ assert.deepEqual(user.lastName, 'Stone');
124
+ });
125
+
126
+ it(`Modifiy the lastName - not in a trasaction - error in the second query`, async () => {
127
+ let error;
128
+ try {
129
+ await withClient(testContext, async (client: Knex.Transaction) => {
130
+ await client(`users`)
131
+ .update({
132
+ lastName: 'Caillou',
133
+ })
134
+ .where(`id`, userId);
135
+
136
+ await client(`NOPE`) // invalid table name
137
+ .update({
138
+ lastName: 'Roquaille',
139
+ })
140
+ .where(`id`, userId);
141
+ });
142
+ } catch (err) {
143
+ error = err;
144
+ }
145
+ if (!error) {
146
+ assert.fail();
147
+ }
148
+
149
+ // ==========================================
150
+ // Has been modified, even if there was an error
151
+ // in the second query!
152
+ // ==========================================
153
+ const user: ITestUser = await getUserById(userId);
154
+ assert.deepEqual(user.lastName, 'Caillou');
155
+ });
156
+
157
+ it(`modifiy the lastName - in a trasaction - error in the second query`, async () => {
158
+ let error;
159
+ try {
160
+ await withTransaction(testContext, async (client: Knex.Transaction) => {
161
+ await client(`users`)
162
+ .update({
163
+ lastName: 'Sable',
164
+ })
165
+ .where(`id`, userId);
166
+
167
+ await client(`NOPE`) // invalid table name
168
+ .update({
169
+ lastName: 'Galet',
170
+ })
171
+ .where(`id`, userId);
172
+ });
173
+ } catch (err) {
174
+ error = err;
175
+ }
176
+ if (!error) {
177
+ assert.fail();
178
+ }
179
+
180
+ // ==========================================
181
+ // Not modified! The transaction has been aborted.
182
+ // ==========================================
183
+ const user: ITestUser = await getUserById(userId);
184
+ assert.deepEqual(user.lastName, 'Caillou');
185
+ });
186
+
187
+ it(`Transaction working even when executing a query in another file/class, by passing the "context" - no error`, async () => {
188
+ await withTransaction(testContext, async (client: Knex.Transaction) => {
189
+ await client(`users`)
190
+ .update({
191
+ lastName: 'Tremblay',
192
+ })
193
+ .where(`id`, userId);
194
+
195
+ // ==========================================
196
+ // Execute a query in another file/class, always
197
+ // by passing the current "context".
198
+ // ==========================================
199
+ await testRepo.changeFirstName(testContext, userId, 'Georges');
200
+ });
201
+
202
+ // ==========================================
203
+ // Modified!
204
+ // ==========================================
205
+ const user: ITestUser = await getUserById(userId);
206
+ assert.deepEqual(user.firstName, 'Georges');
207
+ assert.deepEqual(user.lastName, 'Tremblay');
208
+ });
209
+
210
+ it(`Transaction working (so aborted) when executing a query in another file/class that generates an error`, async () => {
211
+ let error;
212
+ try {
213
+ await withTransaction(testContext, async (client: Knex.Transaction) => {
214
+ await client(`users`)
215
+ .update({
216
+ lastName: 'Lapointe',
217
+ })
218
+ .where(`id`, userId);
219
+
220
+ // ==========================================
221
+ // Execute a query in another file/class. This
222
+ // generated an error which must rollback the
223
+ // transaction!
224
+ // ==========================================
225
+ await testRepo.generateSqlError(testContext);
226
+ });
227
+ } catch (err) {
228
+ error = err;
229
+ }
230
+ if (!error) {
231
+ assert.fail();
232
+ }
233
+
234
+ // ==========================================
235
+ // Not modified! The transaction has been aborted.
236
+ // ==========================================
237
+ const user: ITestUser = await getUserById(userId);
238
+ assert.deepEqual(user.firstName, 'Georges');
239
+ assert.deepEqual(user.lastName, 'Tremblay');
240
+ });
241
+
242
+ it(`Transaction rollbacked in a regular Error too`, async () => {
243
+ let error;
244
+ try {
245
+ await withTransaction(testContext, async (client: Knex.Transaction) => {
246
+ await client(`users`)
247
+ .update({
248
+ lastName: 'Lapointe',
249
+ })
250
+ .where(`id`, userId);
251
+
252
+ throw new Error(`Some error`);
253
+ });
254
+ } catch (err) {
255
+ error = err;
256
+ }
257
+ if (!error) {
258
+ assert.fail();
259
+ }
260
+
261
+ // ==========================================
262
+ // Not modified! The transaction has been aborted.
263
+ // ==========================================
264
+ const user: ITestUser = await getUserById(userId);
265
+ assert.deepEqual(user.firstName, 'Georges');
266
+ assert.deepEqual(user.lastName, 'Tremblay');
267
+ });
268
+
269
+ it(`Retured value, withClient`, async () => {
270
+ const user: ITestUser = await withClient<ITestUser>(
271
+ testContext,
272
+ async (client: Knex.Transaction) => {
273
+ await client(`users`)
274
+ .update({
275
+ lastName: 'aaaaa',
276
+ })
277
+ .where(`id`, userId);
278
+
279
+ return await getUserById(userId);
280
+ },
281
+ );
282
+
283
+ assert.deepEqual(user.lastName, 'aaaaa');
284
+ });
285
+
286
+ it(`Retured value, withTransaction`, async () => {
287
+ const user: ITestUser = await withTransaction<ITestUser>(
288
+ testContext,
289
+ async (client: Knex.Transaction) => {
290
+ await client(`users`)
291
+ .update({
292
+ lastName: 'bbbbb',
293
+ })
294
+ .where(`id`, userId);
295
+
296
+ return await getUserById(userId);
297
+ },
298
+ );
299
+
300
+ assert.deepEqual(user.lastName, 'bbbbb');
301
+ });
302
+ });
@@ -0,0 +1,94 @@
1
+ import { Knex } from 'knex';
2
+ import * as _ from 'lodash';
3
+ import { IDatabaseContext } from './databaseContext';
4
+
5
+ /**
6
+ * A Knex manager that allows nested transactions.
7
+ * Instead of using a knex client directly, always use
8
+ * the "withClient()" and "withTransaction()" provided here...
9
+ * This will make sure that if the query is called inside an
10
+ * already started transaction, it will be part of it.
11
+ */
12
+ export class KnexTransactionManager {
13
+ private getClientFnt: () => Promise<Knex>;
14
+
15
+ /**
16
+ * @param getClientFnt a function to return the
17
+ * knex client to use.
18
+ */
19
+ constructor(getClientFnt: () => Promise<Knex>) {
20
+ this.getClientFnt = getClientFnt;
21
+ }
22
+
23
+ /**
24
+ * Provides a knex client to run queries that need an explicit
25
+ * transaction. If a transaction is already started when this
26
+ * function is called, the same transaction will be used.
27
+ */
28
+ public async withTransaction<T>(
29
+ context: IDatabaseContext,
30
+ fnt: (client: Knex) => Promise<T>,
31
+ ): Promise<T> {
32
+ return await this.withClient(context, fnt, true);
33
+ }
34
+
35
+ /**
36
+ * Provides a knex client to run queries that don't need an explicit
37
+ * transaction. But if a transaction is already started when this
38
+ * function is called, the queries will be part of the transaction.
39
+ */
40
+ public async withClient<T>(
41
+ context: IDatabaseContext,
42
+ fnt: (client: Knex) => Promise<T>,
43
+ transactional = false,
44
+ ): Promise<T> {
45
+ let contextClean = context;
46
+ if (_.isNil(contextClean)) {
47
+ contextClean = {
48
+ currentKnexClient: null,
49
+ };
50
+ }
51
+
52
+ const existingClient: Knex = contextClean.currentKnexClient;
53
+
54
+ if (existingClient && existingClient.name !== 'knex') {
55
+ throw new Error(
56
+ `This manager requires a *knex* client to be passed in the database context! Currently : ${JSON.stringify(
57
+ existingClient,
58
+ null,
59
+ 2,
60
+ )}`,
61
+ );
62
+ }
63
+
64
+ if (existingClient && (!transactional || this.isTransactionClient(existingClient))) {
65
+ return await fnt(existingClient);
66
+ }
67
+
68
+ const client = await this.getClientFnt();
69
+ if (transactional) {
70
+ return await client.transaction(async (trx) => {
71
+ contextClean.currentKnexClient = trx;
72
+ try {
73
+ return await fnt(trx as any);
74
+ } finally {
75
+ contextClean.currentKnexClient = existingClient;
76
+ }
77
+ });
78
+ }
79
+
80
+ contextClean.currentKnexClient = client;
81
+ try {
82
+ return await fnt(client);
83
+ } finally {
84
+ contextClean.currentKnexClient = existingClient;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * "knex.Transaction" type guard.
90
+ */
91
+ protected isTransactionClient = (client: any): client is Knex.Transaction => {
92
+ return client && 'commit' in client && 'rollback' in client;
93
+ };
94
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ ILogger,
3
+ initLogger,
4
+ LazyLogger,
5
+ Logger,
6
+ LoggerConfigs,
7
+ LogLevel,
8
+ } from '@villedemontreal/logger';
9
+ import { configs } from '../config/configs';
10
+
11
+ let testingLoggerLibInitialised = false;
12
+
13
+ /**
14
+ * Creates a Logger.
15
+ */
16
+ export function createLogger(name: string): ILogger {
17
+ // ==========================================
18
+ // We use a LazyLogger so the real Logger
19
+ // is only created when the first
20
+ // log is actually performed... At that point,
21
+ // our "configs.loggerCreator" configuration
22
+ // must have been set by the code using our library!
23
+ //
24
+ // This pattern allows calling code to import
25
+ // modules from us in which a logger is
26
+ // created in the global scope :
27
+ //
28
+ // let logger = createLogger('someName');
29
+ //
30
+ // Without a Lazy Logger, the library configurations
31
+ // would at that moment *not* have been set yet
32
+ // (by the calling code) and an Error would be thrown
33
+ // because the "configs.loggerCreator" is required.
34
+ // ==========================================
35
+ return new LazyLogger(name, (nameArg: string) => {
36
+ return configs.loggerCreator(nameArg);
37
+ });
38
+ }
39
+
40
+ function initTestingLoggerConfigs() {
41
+ const loggerConfig: LoggerConfigs = new LoggerConfigs(() => 'test-cid');
42
+ loggerConfig.setLogLevel(LogLevel.DEBUG);
43
+ initLogger(loggerConfig);
44
+ }
45
+
46
+ /**
47
+ * A Logger that uses a dummy cid provider.
48
+ *
49
+ * Only use this when running the tests!
50
+ */
51
+ export function getTestingLoggerCreator(): (name: string) => ILogger {
52
+ return (name: string): ILogger => {
53
+ if (!testingLoggerLibInitialised) {
54
+ initTestingLoggerConfigs();
55
+ testingLoggerLibInitialised = true;
56
+ }
57
+
58
+ return new Logger(name);
59
+ };
60
+ }
@@ -0,0 +1,13 @@
1
+ import { configs } from '../config/configs';
2
+ import { getTestingLoggerCreator } from '../utils/logger';
3
+
4
+ /**
5
+ * Call this when your need to set
6
+ * *Testing* configurations to the current
7
+ * library, without the need for a calling code
8
+ * to do so.
9
+ *
10
+ */
11
+ export function setTestingConfigurations(): void {
12
+ configs.setLoggerCreator(getTestingLoggerCreator());
13
+ }