1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 | const Transaction = require('../index').Transaction;
|
8 |
|
9 | const chai = require('chai');
|
10 | chai.use(require('chai-as-promised'));
|
11 | const {expect} = chai;
|
12 | const chaiAsPromised = require('chai-as-promised');
|
13 | const testConnector = require('./connectors/test-sql-connector');
|
14 |
|
15 | const juggler = require('loopback-datasource-juggler');
|
16 | let db, Post, Review;
|
17 | describe('transactions', function() {
|
18 | before(function(done) {
|
19 | db = new juggler.DataSource({
|
20 | connector: testConnector,
|
21 | debug: true,
|
22 | });
|
23 | db.once('connected', function() {
|
24 | Post = db.define('PostTX', {
|
25 | title: {type: String, length: 255, index: true},
|
26 | content: {type: String},
|
27 | });
|
28 | Review = db.define('ReviewTX', {
|
29 | author: String,
|
30 | content: {type: String},
|
31 | });
|
32 | Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'});
|
33 | done();
|
34 | });
|
35 | });
|
36 |
|
37 | let currentTx;
|
38 | let hooks = [];
|
39 |
|
40 | function createPostInTx(post, timeout) {
|
41 | return function(done) {
|
42 |
|
43 | Post.beginTransaction({
|
44 | isolationLevel: Transaction.READ_COMMITTED,
|
45 | timeout: timeout,
|
46 | },
|
47 | function(err, tx) {
|
48 | if (err) return done(err);
|
49 | expect(typeof tx.id).to.eql('string');
|
50 | hooks = [];
|
51 | tx.observe('before commit', function(context, next) {
|
52 | hooks.push('before commit');
|
53 | next();
|
54 | });
|
55 | tx.observe('after commit', function(context, next) {
|
56 | hooks.push('after commit');
|
57 | next();
|
58 | });
|
59 | tx.observe('before rollback', function(context, next) {
|
60 | hooks.push('before rollback');
|
61 | next();
|
62 | });
|
63 | tx.observe('after rollback', function(context, next) {
|
64 | hooks.push('after rollback');
|
65 | next();
|
66 | });
|
67 | currentTx = tx;
|
68 | Post.create(post, {transaction: tx, model: 'Post'},
|
69 | function(err, p) {
|
70 | if (err) {
|
71 | done(err);
|
72 | } else {
|
73 | p.reviews.create({
|
74 | author: 'John',
|
75 | content: 'Review for ' + p.title,
|
76 | }, {transaction: tx, model: 'Review'},
|
77 | function(err, c) {
|
78 | done(err);
|
79 | });
|
80 | }
|
81 | });
|
82 | });
|
83 | };
|
84 | }
|
85 |
|
86 |
|
87 |
|
88 | function expectToFindPosts(where, count, inTx) {
|
89 | return function(done) {
|
90 | const options = {model: 'Post'};
|
91 | if (inTx) {
|
92 | options.transaction = currentTx;
|
93 | }
|
94 | Post.find({where: where}, options,
|
95 | function(err, posts) {
|
96 | if (err) return done(err);
|
97 | expect(posts.length).to.be.eql(count);
|
98 |
|
99 | Post.count(where, options,
|
100 | function(err, result) {
|
101 | if (err) return done(err);
|
102 | expect(result).to.be.eql(count);
|
103 | if (count) {
|
104 |
|
105 | options.model = 'Review';
|
106 |
|
107 |
|
108 | posts[0].reviews({}, options, function(err, reviews) {
|
109 | if (err) return done(err);
|
110 | expect(reviews.length).to.be.eql(count);
|
111 | done();
|
112 | });
|
113 | } else {
|
114 | done();
|
115 | }
|
116 | });
|
117 | });
|
118 | };
|
119 | }
|
120 |
|
121 | describe('commit', function() {
|
122 | const post = {title: 't1', content: 'c1'};
|
123 | before(createPostInTx(post));
|
124 |
|
125 | it('should not see the uncommitted insert', expectToFindPosts(post, 0));
|
126 |
|
127 | it('should see the uncommitted insert from the same transaction',
|
128 | expectToFindPosts(post, 1, true));
|
129 |
|
130 | it('should commit a transaction', function(done) {
|
131 | currentTx.commit(function(err) {
|
132 | expect(hooks).to.eql(['before commit', 'after commit']);
|
133 | done(err);
|
134 | });
|
135 | });
|
136 |
|
137 | it('should see the committed insert', expectToFindPosts(post, 1));
|
138 |
|
139 | it('should report error if the transaction is not active', function(done) {
|
140 | currentTx.commit(function(err) {
|
141 | expect(err).to.be.instanceof(Error);
|
142 | done();
|
143 | });
|
144 | });
|
145 | });
|
146 |
|
147 | describe('rollback', function() {
|
148 | before(function() {
|
149 |
|
150 | db.connector.data = {};
|
151 | });
|
152 |
|
153 | const post = {title: 't2', content: 'c2'};
|
154 | before(createPostInTx(post));
|
155 |
|
156 | it('should not see the uncommitted insert', expectToFindPosts(post, 0));
|
157 |
|
158 | it('should see the uncommitted insert from the same transaction',
|
159 | expectToFindPosts(post, 1, true));
|
160 |
|
161 | it('should rollback a transaction', function(done) {
|
162 | currentTx.rollback(function(err) {
|
163 | expect(hooks).to.eql(['before rollback', 'after rollback']);
|
164 | done(err);
|
165 | });
|
166 | });
|
167 |
|
168 | it('should not see the rolledback insert', expectToFindPosts(post, 0));
|
169 |
|
170 | it('should report error if the transaction is not active', function(done) {
|
171 | currentTx.rollback(function(err) {
|
172 | expect(err).to.be.instanceof(Error);
|
173 | done();
|
174 | });
|
175 | });
|
176 | });
|
177 |
|
178 | describe('timeout', function() {
|
179 | const TIMEOUT = 50;
|
180 | before(function() {
|
181 |
|
182 | db.connector.data = {};
|
183 | });
|
184 |
|
185 | const post = {title: 't3', content: 'c3'};
|
186 | beforeEach(createPostInTx(post, TIMEOUT));
|
187 |
|
188 | it('should report timeout', function(done) {
|
189 |
|
190 | setTimeout(runTheTest, TIMEOUT * 3);
|
191 |
|
192 | function runTheTest() {
|
193 | Post.find({where: {title: 't3'}}, {transaction: currentTx},
|
194 | function(err, posts) {
|
195 | expect(err).to.match(/transaction.*not active/);
|
196 | done();
|
197 | });
|
198 | }
|
199 | });
|
200 |
|
201 | it('should invoke the timeout hook', function(done) {
|
202 | currentTx.observe('timeout', function(context, next) {
|
203 | next();
|
204 | done();
|
205 | });
|
206 |
|
207 |
|
208 |
|
209 | this.timeout(TIMEOUT * 3);
|
210 | });
|
211 | });
|
212 |
|
213 | describe('isActive', function() {
|
214 | it('returns true when connection is active', function(done) {
|
215 | Post.beginTransaction({
|
216 | isolationLevel: Transaction.READ_COMMITTED,
|
217 | timeout: 1000,
|
218 | },
|
219 | function(err, tx) {
|
220 | if (err) return done(err);
|
221 | expect(tx.isActive()).to.equal(true);
|
222 | return done();
|
223 | });
|
224 | });
|
225 | it('returns false when connection is not active', function(done) {
|
226 | Post.beginTransaction({
|
227 | isolationLevel: Transaction.READ_COMMITTED,
|
228 | timeout: 1000,
|
229 | },
|
230 | function(err, tx) {
|
231 | if (err) return done(err);
|
232 | delete tx.connection;
|
233 | expect(tx.isActive()).to.equal(false);
|
234 | return done();
|
235 | });
|
236 | });
|
237 | });
|
238 |
|
239 | describe('transaction instance', function() {
|
240 | function TestTransaction(connector, connection) {
|
241 | this.connector = connector;
|
242 | this.connection = connection;
|
243 | }
|
244 | Object.assign(TestTransaction.prototype, Transaction.prototype);
|
245 | TestTransaction.prototype.foo = true;
|
246 | function beginTransaction(isolationLevel, cb) {
|
247 | return cb(null, new TestTransaction(testConnector, {}));
|
248 | }
|
249 |
|
250 | it('should do nothing when transaction is like a Transaction', function(done) {
|
251 | testConnector.initialize(db, function(err, resultConnector) {
|
252 | resultConnector.beginTransaction = beginTransaction;
|
253 | Transaction.begin(resultConnector, Transaction.READ_COMMITTED,
|
254 | function(err, result) {
|
255 | if (err) done(err);
|
256 | expect(result).to.be.instanceof(TestTransaction);
|
257 | expect(result.foo).to.equal(true);
|
258 | done();
|
259 | });
|
260 | });
|
261 | });
|
262 |
|
263 | it('should create new instance when transaction is not like a Transaction',
|
264 | function(done) {
|
265 | testConnector.initialize(db, function(err, resultConnector) {
|
266 | resultConnector.beginTransaction = beginTransaction;
|
267 | delete TestTransaction.prototype.commit;
|
268 | Transaction.begin(resultConnector, Transaction.READ_COMMITTED,
|
269 | function(err, result) {
|
270 | if (err) done(err);
|
271 | expect(result).to.not.be.instanceof(TestTransaction);
|
272 | expect(result).to.be.instanceof(Transaction);
|
273 | expect(result.foo).to.equal(undefined);
|
274 | done();
|
275 | });
|
276 | });
|
277 | });
|
278 | });
|
279 |
|
280 | it('can return promise for commit', function() {
|
281 | const connectorObject = {};
|
282 | connectorObject.commit = function(connection, cb) {
|
283 | return cb(null, 'committed');
|
284 | };
|
285 | const transactionInstance = new Transaction(connectorObject, {});
|
286 | return expect(transactionInstance.commit()).to.eventually.equal('committed');
|
287 | });
|
288 |
|
289 | it('can return promise for rollback', function() {
|
290 | const connectorObject = {};
|
291 | connectorObject.rollback = function(connection, cb) {
|
292 | return cb(null, 'rolledback');
|
293 | };
|
294 | const transactionInstance = new Transaction(connectorObject, {});
|
295 | return expect(transactionInstance.rollback()).to.eventually.equal('rolledback');
|
296 | });
|
297 |
|
298 | it('can return promise for begin', function() {
|
299 | const connectorObject = {};
|
300 | connectorObject.beginTransaction = function(connection, cb) {
|
301 | return cb(null, 'begun');
|
302 | };
|
303 |
|
304 | return expect(
|
305 | Transaction.begin(connectorObject, ''),
|
306 | ).to.eventually.be.instanceOf(Transaction);
|
307 | });
|
308 | });
|