UNPKG

6.13 kBJavaScriptView Raw
1// Copyright IBM Corp. 2017,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7/* global getSchema:false */
8const DataSource = require('..').DataSource;
9const EventEmitter = require('events');
10const Connector = require('loopback-connector').Connector;
11const Transaction = require('loopback-connector').Transaction;
12const should = require('./init.js');
13
14describe('Transactions on memory connector', function() {
15 let db, tx;
16
17 before(() => {
18 db = getSchema();
19 db.define('Model');
20 });
21
22 it('returns an EventEmitter object', done => {
23 tx = db.transaction();
24 tx.should.be.instanceOf(EventEmitter);
25 done();
26 });
27
28 it('exposes and caches slave models', done => {
29 testModelCaching(tx.models, db.models);
30 done();
31 });
32
33 it('changes count when committing', done => {
34 db.models.Model.count((err, count) => {
35 should.not.exist(err);
36 should.exist(count);
37 count.should.equal(0);
38 tx.models.Model.create(Array(1), () => {
39 // Only called after tx.commit()!
40 });
41 tx.commit(err => {
42 should.not.exist(err);
43 db.models.Model.count((err, count) => {
44 should.not.exist(err);
45 should.exist(count);
46 count.should.equal(1);
47 done();
48 });
49 });
50 });
51 });
52});
53
54describe('Transactions on test connector without execute()', () => {
55 let db, tx;
56
57 before(() => {
58 db = createDataSource();
59 });
60
61 beforeEach(resetState);
62
63 it('resolves to an EventEmitter', done => {
64 const promise = db.transaction();
65 promise.should.be.Promise();
66 promise.then(transaction => {
67 should.exist(transaction);
68 transaction.should.be.instanceof(EventEmitter);
69 tx = transaction;
70 done();
71 }, done);
72 });
73
74 it('beginTransaction returns a transaction', async () => {
75 const promise = db.beginTransaction(Transaction.READ_UNCOMMITTED);
76 promise.should.be.Promise();
77 const transaction = await promise;
78 transaction.should.be.instanceof(EventEmitter);
79 });
80
81 it('exposes and caches slave models', done => {
82 testModelCaching(tx.models, db.models);
83 done();
84 });
85
86 it('does not allow nesting of transactions', done => {
87 (() => tx.transaction()).should.throw('Nesting transactions is not supported');
88 done();
89 });
90
91 it('calls commit() on the connector', done => {
92 db.transaction().then(tx => {
93 tx.commit(err => {
94 callCount.should.deepEqual({commit: 1, rollback: 0, create: 0});
95 done(err);
96 });
97 }, done);
98 });
99
100 it('calls rollback() on the connector', done => {
101 db.transaction().then(tx => {
102 tx.rollback(err => {
103 callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
104 done(err);
105 });
106 }, done);
107 });
108});
109
110describe('Transactions on test connector with execute()', () => {
111 let db;
112
113 before(() => {
114 db = createDataSource();
115 });
116
117 beforeEach(resetState);
118
119 it('passes models and calls commit() automatically', done => {
120 db.transaction(models => {
121 testModelCaching(models, db.models);
122 return models.Model.create({});
123 }, err => {
124 callCount.should.deepEqual({commit: 1, rollback: 0, create: 1});
125 transactionPassed.should.be.true();
126 done(err);
127 });
128 });
129
130 it('calls rollback() automatically when throwing an error', done => {
131 let error;
132 db.transaction(models => {
133 error = new Error('exception');
134 throw error;
135 }, err => {
136 error.should.equal(err);
137 callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
138 done();
139 });
140 });
141
142 it('reports execution timeouts', done => {
143 let timedOut = false;
144 db.transaction(models => {
145 setTimeout(() => {
146 models.Model.create({}, function(err) {
147 if (!timedOut) {
148 done(new Error('Timeout was ineffective'));
149 } else {
150 should.exist(err);
151 err.message.should.startWith('The transaction is not active:');
152 done();
153 }
154 });
155 }, 50);
156 }, {
157 timeout: 25,
158 }, err => {
159 timedOut = true;
160 should.exist(err);
161 err.code.should.equal('TRANSACTION_TIMEOUT');
162 err.message.should.equal('Transaction is rolled back due to timeout');
163 callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
164 });
165 });
166});
167
168function createDataSource() {
169 const db = new DataSource({
170 initialize: (dataSource, cb) => {
171 dataSource.connector = new TestConnector();
172 cb();
173 },
174 });
175 db.define('Model');
176 return db;
177}
178
179function testModelCaching(txModels, dbModels) {
180 should.exist(txModels);
181 // Test models caching mechanism:
182 // Model property should be a accessor with a getter first:
183 const accessor = Object.getOwnPropertyDescriptor(txModels, 'Model');
184 should.exist(accessor);
185 should.exist(accessor.get);
186 accessor.get.should.be.Function();
187 const Model = txModels.Model;
188 should.exist(Model);
189 // After accessing it once, it should be a normal cached property:
190 const desc = Object.getOwnPropertyDescriptor(txModels, 'Model');
191 should.exist(desc.value);
192 Model.should.equal(txModels.Model);
193 Model.prototype.should.be.instanceof(dbModels.Model);
194}
195
196let callCount;
197let transactionPassed;
198
199function resetState() {
200 callCount = {commit: 0, rollback: 0, create: 0};
201 transactionPassed = false;
202}
203
204class TestConnector extends Connector {
205 constructor() {
206 super('test');
207 }
208
209 beginTransaction(isolationLevel, cb) {
210 this.currentTransaction = new Transaction(this, this);
211 process.nextTick(() => cb(null, this.currentTransaction));
212 }
213
214 commit(tx, cb) {
215 callCount.commit++;
216 cb();
217 }
218
219 rollback(tx, cb) {
220 callCount.rollback++;
221 cb();
222 }
223
224 create(model, data, options, cb) {
225 callCount.create++;
226 const transaction = options.transaction;
227 const current = this.currentTransaction;
228 transactionPassed = transaction &&
229 (current === transaction || current === transaction.connection);
230 cb();
231 }
232}