@storyous/mongodb 1.0.0
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 +89 -0
- package/__tests__/appData.test.js +36 -0
- package/__tests__/appData.test.ts +39 -0
- package/__tests__/fixtures.test.js +40 -0
- package/__tests__/fixtures.test.ts +44 -0
- package/__tests__/getMongoClient.js +19 -0
- package/__tests__/getMongoClient.ts +24 -0
- package/__tests__/migrations/1642152064249-test.js +7 -0
- package/__tests__/migrations/1642152064249-test.ts +7 -0
- package/__tests__/mongoLocker.test.js +151 -0
- package/__tests__/mongoLocker.test.ts +191 -0
- package/__tests__/runMigration.test.js +22 -0
- package/__tests__/runMigration.test.ts +23 -0
- package/__tests__/withTransaction.test.js +40 -0
- package/__tests__/withTransaction.test.ts +67 -0
- package/lib/appData/index.js +40 -0
- package/lib/appData/index.ts +48 -0
- package/lib/concurrentTask.js +20 -0
- package/lib/concurrentTask.ts +30 -0
- package/lib/fixtures.js +28 -0
- package/lib/fixtures.ts +36 -0
- package/lib/getCollection.js +8 -0
- package/lib/getCollection.ts +4 -0
- package/lib/getMongoLocker.js +53 -0
- package/lib/getMongoLocker.ts +71 -0
- package/lib/index.js +27 -0
- package/lib/index.ts +25 -0
- package/lib/migrations/Migration.js +22 -0
- package/lib/migrations/Migration.ts +29 -0
- package/lib/migrations/MigrationsStore.js +65 -0
- package/lib/migrations/MigrationsStore.ts +73 -0
- package/lib/migrations/runMigrations.js +50 -0
- package/lib/migrations/runMigrations.ts +57 -0
- package/lib/migrations/template.js +7 -0
- package/lib/migrations/template.ts +6 -0
- package/lib/mongoClient.js +18 -0
- package/lib/mongoClient.ts +25 -0
- package/lib/mongoErrorCodes.js +11 -0
- package/lib/mongoErrorCodes.ts +10 -0
- package/lib/mongoLocker.js +17 -0
- package/lib/mongoLocker.ts +15 -0
- package/lib/withTransaction.js +21 -0
- package/lib/withTransaction.ts +24 -0
- package/package.json +40 -0
- package/tsconfig.json +14 -0
- package/types/migrate/index.d.ts +101 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { noop, pick, times } from 'lodash';
|
|
3
|
+
import { beforeEach, describe, it } from 'mocha';
|
|
4
|
+
import { Collection, MongoClient } from 'mongodb';
|
|
5
|
+
import sinon from 'sinon';
|
|
6
|
+
import {
|
|
7
|
+
getMongoLocker, LockCallback, LockerOptions, mongoLocker,
|
|
8
|
+
} from '../lib';
|
|
9
|
+
import { Key } from '../lib/mongoClient';
|
|
10
|
+
import connectAndGetMongoClient from './getMongoClient';
|
|
11
|
+
|
|
12
|
+
describe('packages/mongodb/mongoLocker', () => {
|
|
13
|
+
|
|
14
|
+
let mongoClient: MongoClient;
|
|
15
|
+
let collection: Collection;
|
|
16
|
+
const collectionName = 'customLocks';
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
mongoClient = await connectAndGetMongoClient();
|
|
20
|
+
collection = mongoClient.db().collection(collectionName);
|
|
21
|
+
await collection.drop().catch(noop);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
[
|
|
25
|
+
{ description: 'default mongoLocker instance', locker: () => mongoLocker },
|
|
26
|
+
{ description: 'custom getMongoLocker', locker: () => getMongoLocker(collection) },
|
|
27
|
+
]
|
|
28
|
+
.forEach((suite) => {
|
|
29
|
+
|
|
30
|
+
describe(suite.description, () => {
|
|
31
|
+
|
|
32
|
+
let locker: (key: Key, callback: LockCallback<any>, options?: LockerOptions) => Promise<any>;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
locker = await suite.locker();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should acquire and release a lock', async () => {
|
|
39
|
+
|
|
40
|
+
let i = 0;
|
|
41
|
+
const callback: any = sinon.spy(() => ++i);
|
|
42
|
+
|
|
43
|
+
const result1 = await locker('AAA', callback);
|
|
44
|
+
assert.strictEqual(result1, 1);
|
|
45
|
+
|
|
46
|
+
const result2 = await locker('AAA', callback);
|
|
47
|
+
assert.strictEqual(result2, 2);
|
|
48
|
+
|
|
49
|
+
assert.deepStrictEqual(callback.callCount, 2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should not be possible to acquire the lock twice at once', async () => {
|
|
53
|
+
|
|
54
|
+
const callback = sinon.spy(() => new Promise((resolve) => { setTimeout(resolve, 50); }));
|
|
55
|
+
const callLocker = () => locker('AAA', callback, { noLaterThan: 0 });
|
|
56
|
+
|
|
57
|
+
const concurrency = 10;
|
|
58
|
+
const promises = times(concurrency, callLocker);
|
|
59
|
+
|
|
60
|
+
// Node 10 or less does not support allSettled. Going through good old cycle in such case
|
|
61
|
+
if (Promise.allSettled) {
|
|
62
|
+
await Promise.allSettled(promises); // just to get rid of UnhandledRejectionErrors
|
|
63
|
+
} else {
|
|
64
|
+
// just to get rid of UnhandledRejectionErrors
|
|
65
|
+
promises.forEach((promise) => promise.catch(() => {}));
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < promises.length; i++) {
|
|
68
|
+
try {
|
|
69
|
+
await promises[i]; // eslint-disable-line
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// That's ok, we expect there can be errors
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const stats = {
|
|
77
|
+
resolved: 0,
|
|
78
|
+
failed: 0,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const promise of promises) {
|
|
82
|
+
try {
|
|
83
|
+
await promise; // eslint-disable-line no-await-in-loop
|
|
84
|
+
stats.resolved++;
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
assert(/The lock is already acquired./.test(err?.message));
|
|
87
|
+
stats.failed++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assert.deepStrictEqual(callback.callCount, 1, 'The callback should be called only once');
|
|
92
|
+
assert.deepStrictEqual(stats, {
|
|
93
|
+
resolved: 1,
|
|
94
|
+
failed: concurrency - 1,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should propagate an error and release the lock if the callback fails', async () => {
|
|
99
|
+
|
|
100
|
+
await assert.rejects(() => locker('AAA', async () => {
|
|
101
|
+
throw new Error('Some error occurred');
|
|
102
|
+
}), /Some error occurred/);
|
|
103
|
+
|
|
104
|
+
const callback = sinon.spy();
|
|
105
|
+
await locker('AAA', callback);
|
|
106
|
+
|
|
107
|
+
assert.strictEqual(callback.callCount, 1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it(
|
|
111
|
+
'should be able to handle few concurrent tasks by waiting some time and acquire retries',
|
|
112
|
+
async () => {
|
|
113
|
+
const callTimes: Date[] = [];
|
|
114
|
+
const callback = sinon.spy(async () => {
|
|
115
|
+
callTimes.push(new Date());
|
|
116
|
+
await new Promise((resolve) => { setTimeout(resolve, 150); });
|
|
117
|
+
callTimes.push(new Date());
|
|
118
|
+
}); // default retryDelay is 200
|
|
119
|
+
|
|
120
|
+
const callLocker = () => locker('AAA', callback);
|
|
121
|
+
|
|
122
|
+
await Promise.all(times(3, callLocker));
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(callback.callCount, 3);
|
|
125
|
+
callTimes.forEach((time, i) => {
|
|
126
|
+
if (i > 0) {
|
|
127
|
+
assert(time > callTimes[i - 1], 'There was a parallel callback execution!');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should be possible to override a lock which expired yet', async () => {
|
|
133
|
+
const expireIn = 500;
|
|
134
|
+
const array: number[] = [];
|
|
135
|
+
const key = 'AAA';
|
|
136
|
+
|
|
137
|
+
const firstCallPromise = locker(key, async () => {
|
|
138
|
+
array.push(1);
|
|
139
|
+
// the method last too long
|
|
140
|
+
await new Promise((resolve) => { setTimeout(resolve, 2 * expireIn); });
|
|
141
|
+
array.push(3);
|
|
142
|
+
}, { expireIn });
|
|
143
|
+
|
|
144
|
+
await new Promise((resolve) => { setTimeout(resolve, expireIn); });
|
|
145
|
+
|
|
146
|
+
const secondCallPromise = locker('AAA', async () => {
|
|
147
|
+
array.push(2);
|
|
148
|
+
}, { expireIn });
|
|
149
|
+
|
|
150
|
+
await Promise.all([firstCallPromise, secondCallPromise]);
|
|
151
|
+
assert.deepStrictEqual(array, [1, 2, 3]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('getMongoLocker', () => {
|
|
157
|
+
it('should create and TTL index on the right field', async () => {
|
|
158
|
+
await mongoClient.db()
|
|
159
|
+
.createCollection(collectionName);
|
|
160
|
+
|
|
161
|
+
let indexes = await collection.indexes();
|
|
162
|
+
assert.deepStrictEqual(indexes.length, 1); // ensure there is only mandatory _id index
|
|
163
|
+
|
|
164
|
+
const locker = await getMongoLocker(collection);
|
|
165
|
+
indexes = await collection.indexes();
|
|
166
|
+
assert.deepStrictEqual(indexes.length, 2, 'No or wrong indexes has been created!');
|
|
167
|
+
assert.deepStrictEqual(pick(indexes[1], 'key', 'expireAfterSeconds'), {
|
|
168
|
+
expireAfterSeconds: 0,
|
|
169
|
+
key: {
|
|
170
|
+
expiresAt: 1,
|
|
171
|
+
},
|
|
172
|
+
}, 'No or wrong indexes has been created!');
|
|
173
|
+
|
|
174
|
+
const startedAt = new Date();
|
|
175
|
+
const someKey = 'someKey';
|
|
176
|
+
const expireIn = 55000;
|
|
177
|
+
await locker(someKey, async () => {
|
|
178
|
+
const document = await collection.findOne({});
|
|
179
|
+
assert.strictEqual(document?._id, someKey);
|
|
180
|
+
assert(
|
|
181
|
+
document.expiresAt >= new Date(startedAt.getTime() + expireIn),
|
|
182
|
+
'The expiresAt property has to be greater then startedAt + expiresIn',
|
|
183
|
+
);
|
|
184
|
+
assert(
|
|
185
|
+
document.expiresAt < new Date(Date.now() + expireIn),
|
|
186
|
+
'The acquiredAt property has to be younger then present + expiresIn',
|
|
187
|
+
);
|
|
188
|
+
}, { expireIn });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const assert_1 = __importDefault(require("assert"));
|
|
7
|
+
const mocha_1 = require("mocha");
|
|
8
|
+
const winston_1 = require("winston");
|
|
9
|
+
const lib_1 = require("../lib");
|
|
10
|
+
(0, mocha_1.describe)('packages/mongodb/migrations', () => {
|
|
11
|
+
(0, mocha_1.before)(async () => {
|
|
12
|
+
lib_1.mongoClient.init('mongodb://127.0.0.1');
|
|
13
|
+
await lib_1.mongoClient.getClient().connect();
|
|
14
|
+
await lib_1.appData._collection.deleteOne({ _id: 'migrations' });
|
|
15
|
+
});
|
|
16
|
+
(0, mocha_1.it)('should runMigrations works', async () => {
|
|
17
|
+
await (0, lib_1.runMigrations)((0, winston_1.createLogger)(), `${__dirname}/migrations`, { safeMigration: false });
|
|
18
|
+
const migration = await lib_1.appData.getDocument('migrations');
|
|
19
|
+
assert_1.default.deepStrictEqual(migration?.lastRun, '1642152064249-test.js');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVuTWlncmF0aW9uLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJydW5NaWdyYXRpb24udGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLG9EQUE0QjtBQUM1QixpQ0FBNkM7QUFDN0MscUNBQXVDO0FBQ3ZDLGdDQUE2RDtBQUU3RCxJQUFBLGdCQUFRLEVBQUMsNkJBQTZCLEVBQUUsR0FBRyxFQUFFO0lBQ3pDLElBQUEsY0FBTSxFQUFDLEtBQUssSUFBSSxFQUFFO1FBQ2QsaUJBQVcsQ0FBQyxJQUFJLENBQUMscUJBQXFCLENBQUMsQ0FBQztRQUN4QyxNQUFNLGlCQUFXLENBQUMsU0FBUyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDeEMsTUFBTSxhQUFPLENBQUMsV0FBVyxDQUFDLFNBQVMsQ0FBQyxFQUFFLEdBQUcsRUFBRSxZQUFZLEVBQUUsQ0FBQyxDQUFDO0lBQy9ELENBQUMsQ0FBQyxDQUFDO0lBRUgsSUFBQSxVQUFFLEVBQUMsNEJBQTRCLEVBQUUsS0FBSyxJQUFJLEVBQUU7UUFDeEMsTUFBTSxJQUFBLG1CQUFhLEVBQ2YsSUFBQSxzQkFBWSxHQUFFLEVBQ2QsR0FBRyxTQUFTLGFBQWEsRUFDekIsRUFBRSxhQUFhLEVBQUUsS0FBSyxFQUFFLENBQzNCLENBQUM7UUFFRixNQUFNLFNBQVMsR0FBRyxNQUFNLGFBQU8sQ0FBQyxXQUFXLENBQUMsWUFBWSxDQUFDLENBQUM7UUFDMUQsZ0JBQU0sQ0FBQyxlQUFlLENBQUMsU0FBUyxFQUFFLE9BQU8sRUFBRSx1QkFBdUIsQ0FBQyxDQUFDO0lBQ3hFLENBQUMsQ0FBQyxDQUFDO0FBQ1AsQ0FBQyxDQUFDLENBQUMifQ==
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { before, describe, it } from 'mocha';
|
|
3
|
+
import { createLogger } from 'winston';
|
|
4
|
+
import { appData, mongoClient, runMigrations } from '../lib';
|
|
5
|
+
|
|
6
|
+
describe('packages/mongodb/migrations', () => {
|
|
7
|
+
before(async () => {
|
|
8
|
+
mongoClient.init('mongodb://127.0.0.1');
|
|
9
|
+
await mongoClient.getClient().connect();
|
|
10
|
+
await appData._collection.deleteOne({ _id: 'migrations' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should runMigrations works', async () => {
|
|
14
|
+
await runMigrations(
|
|
15
|
+
createLogger(),
|
|
16
|
+
`${__dirname}/migrations`,
|
|
17
|
+
{ safeMigration: false },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const migration = await appData.getDocument('migrations');
|
|
21
|
+
assert.deepStrictEqual(migration?.lastRun, '1642152064249-test.js');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const assert_1 = __importDefault(require("assert"));
|
|
7
|
+
const lodash_1 = require("lodash");
|
|
8
|
+
const mocha_1 = require("mocha");
|
|
9
|
+
const lib_1 = require("../lib");
|
|
10
|
+
require('./getMongoClient');
|
|
11
|
+
(0, mocha_1.describe)('packages/mongodb/withTransaction', () => {
|
|
12
|
+
let testCollection;
|
|
13
|
+
(0, mocha_1.beforeEach)(async () => {
|
|
14
|
+
testCollection = (0, lib_1.getCollection)('withTransactionTests');
|
|
15
|
+
await testCollection.drop().catch(lodash_1.noop);
|
|
16
|
+
await testCollection.insertOne({}); // ensure the collection is created before tests
|
|
17
|
+
});
|
|
18
|
+
(0, mocha_1.it)('should commit the transaction', async () => {
|
|
19
|
+
const expectedReturnValue = { some: 'value' };
|
|
20
|
+
const returnValue = await (0, lib_1.withTransaction)(async (session) => {
|
|
21
|
+
await testCollection.insertOne({}, { session });
|
|
22
|
+
await testCollection.insertOne({}, { session });
|
|
23
|
+
assert_1.default.equal(await testCollection.countDocuments({}, { session }), 3);
|
|
24
|
+
return expectedReturnValue;
|
|
25
|
+
});
|
|
26
|
+
assert_1.default.equal(await testCollection.countDocuments({}), 3, 'there should available documents after a commit');
|
|
27
|
+
assert_1.default.equal(returnValue, expectedReturnValue);
|
|
28
|
+
});
|
|
29
|
+
(0, mocha_1.it)('should rollback the transaction', async () => {
|
|
30
|
+
const error = new Error('some error');
|
|
31
|
+
await assert_1.default.rejects(() => (0, lib_1.withTransaction)(async (session) => {
|
|
32
|
+
await testCollection.insertOne({}, { session });
|
|
33
|
+
await testCollection.insertOne({}, { session });
|
|
34
|
+
assert_1.default.equal(await testCollection.countDocuments({}, { session }), 3);
|
|
35
|
+
throw error;
|
|
36
|
+
}), error);
|
|
37
|
+
assert_1.default.equal(await testCollection.countDocuments({}), 1, 'there should be no document after a rollback');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2l0aFRyYW5zYWN0aW9uLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ3aXRoVHJhbnNhY3Rpb24udGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLG9EQUE0QjtBQUM1QixtQ0FBOEI7QUFDOUIsaUNBQWlEO0FBRWpELGdDQUF3RDtBQUV4RCxPQUFPLENBQUMsa0JBQWtCLENBQUMsQ0FBQztBQUU1QixJQUFBLGdCQUFRLEVBQUMsa0NBQWtDLEVBQUUsR0FBRyxFQUFFO0lBRTlDLElBQUksY0FBMEIsQ0FBQztJQUMvQixJQUFBLGtCQUFVLEVBQUMsS0FBSyxJQUFJLEVBQUU7UUFDbEIsY0FBYyxHQUFHLElBQUEsbUJBQWEsRUFBQyxzQkFBc0IsQ0FBQyxDQUFDO1FBQ3ZELE1BQU0sY0FBYyxDQUFDLElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxhQUFJLENBQUMsQ0FBQztRQUN4QyxNQUFNLGNBQWMsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFRLENBQUMsQ0FBQyxnREFBZ0Q7SUFDL0YsQ0FBQyxDQUFDLENBQUM7SUFFSCxJQUFBLFVBQUUsRUFBQywrQkFBK0IsRUFBRSxLQUFLLElBQUksRUFBRTtRQUUzQyxNQUFNLG1CQUFtQixHQUFHLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxDQUFDO1FBRTlDLE1BQU0sV0FBVyxHQUFHLE1BQU0sSUFBQSxxQkFBZSxFQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtZQUV4RCxNQUFNLGNBQWMsQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztZQUNoRCxNQUFNLGNBQWMsQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztZQUVoRCxnQkFBTSxDQUFDLEtBQUssQ0FDUixNQUFNLGNBQWMsQ0FBQyxjQUFjLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsRUFDcEQsQ0FBQyxDQUNKLENBQUM7WUFFRixPQUFPLG1CQUFtQixDQUFDO1FBQy9CLENBQUMsQ0FBQyxDQUFDO1FBRUgsZ0JBQU0sQ0FBQyxLQUFLLENBQ1IsTUFBTSxjQUFjLENBQUMsY0FBYyxDQUFDLEVBQUUsQ0FBQyxFQUN2QyxDQUFDLEVBQ0QsaURBQWlELENBQ3BELENBQUM7UUFFRixnQkFBTSxDQUFDLEtBQUssQ0FBQyxXQUFXLEVBQUUsbUJBQW1CLENBQUMsQ0FBQztJQUNuRCxDQUFDLENBQUMsQ0FBQztJQUVILElBQUEsVUFBRSxFQUFDLGlDQUFpQyxFQUFFLEtBQUssSUFBSSxFQUFFO1FBQzdDLE1BQU0sS0FBSyxHQUFHLElBQUksS0FBSyxDQUFDLFlBQVksQ0FBQyxDQUFDO1FBRXRDLE1BQU0sZ0JBQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBQSxxQkFBZSxFQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtZQUV6RCxNQUFNLGNBQWMsQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztZQUNoRCxNQUFNLGNBQWMsQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztZQUVoRCxnQkFBTSxDQUFDLEtBQUssQ0FDUixNQUFNLGNBQWMsQ0FBQyxjQUFjLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsRUFDcEQsQ0FBQyxDQUNKLENBQUM7WUFFRixNQUFNLEtBQUssQ0FBQztRQUVoQixDQUFDLENBQUMsRUFBRSxLQUFLLENBQUMsQ0FBQztRQUVYLGdCQUFNLENBQUMsS0FBSyxDQUNSLE1BQU0sY0FBYyxDQUFDLGNBQWMsQ0FBQyxFQUFFLENBQUMsRUFDdkMsQ0FBQyxFQUNELDhDQUE4QyxDQUNqRCxDQUFDO0lBQ04sQ0FBQyxDQUFDLENBQUM7QUFDUCxDQUFDLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { noop } from 'lodash';
|
|
3
|
+
import { beforeEach, describe, it } from 'mocha';
|
|
4
|
+
import { Collection } from 'mongodb';
|
|
5
|
+
import { getCollection, withTransaction } from '../lib';
|
|
6
|
+
|
|
7
|
+
require('./getMongoClient');
|
|
8
|
+
|
|
9
|
+
describe('packages/mongodb/withTransaction', () => {
|
|
10
|
+
|
|
11
|
+
let testCollection: Collection;
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
testCollection = getCollection('withTransactionTests');
|
|
14
|
+
await testCollection.drop().catch(noop);
|
|
15
|
+
await testCollection.insertOne({}) as any; // ensure the collection is created before tests
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should commit the transaction', async () => {
|
|
19
|
+
|
|
20
|
+
const expectedReturnValue = { some: 'value' };
|
|
21
|
+
|
|
22
|
+
const returnValue = await withTransaction(async (session) => {
|
|
23
|
+
|
|
24
|
+
await testCollection.insertOne({}, { session });
|
|
25
|
+
await testCollection.insertOne({}, { session });
|
|
26
|
+
|
|
27
|
+
assert.equal(
|
|
28
|
+
await testCollection.countDocuments({}, { session }),
|
|
29
|
+
3,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return expectedReturnValue;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(
|
|
36
|
+
await testCollection.countDocuments({}),
|
|
37
|
+
3,
|
|
38
|
+
'there should available documents after a commit',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
assert.equal(returnValue, expectedReturnValue);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should rollback the transaction', async () => {
|
|
45
|
+
const error = new Error('some error');
|
|
46
|
+
|
|
47
|
+
await assert.rejects(() => withTransaction(async (session) => {
|
|
48
|
+
|
|
49
|
+
await testCollection.insertOne({}, { session });
|
|
50
|
+
await testCollection.insertOne({}, { session });
|
|
51
|
+
|
|
52
|
+
assert.equal(
|
|
53
|
+
await testCollection.countDocuments({}, { session }),
|
|
54
|
+
3,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
throw error;
|
|
58
|
+
|
|
59
|
+
}), error);
|
|
60
|
+
|
|
61
|
+
assert.equal(
|
|
62
|
+
await testCollection.countDocuments({}),
|
|
63
|
+
1,
|
|
64
|
+
'there should be no document after a rollback',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const getCollection_1 = __importDefault(require("../getCollection"));
|
|
7
|
+
const mongoDuplicateKeyErrorCodes = [11000, 11001];
|
|
8
|
+
const appData = {
|
|
9
|
+
get _collection() {
|
|
10
|
+
return (0, getCollection_1.default)('appData');
|
|
11
|
+
},
|
|
12
|
+
_failReads: 0,
|
|
13
|
+
ensureDocumentExists(document) {
|
|
14
|
+
return this._collection
|
|
15
|
+
.insertOne(document)
|
|
16
|
+
.catch((err) => (new Promise((resolve, reject) => {
|
|
17
|
+
if (!mongoDuplicateKeyErrorCodes.includes(err.code)) {
|
|
18
|
+
reject(err);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
resolve(null);
|
|
22
|
+
})));
|
|
23
|
+
},
|
|
24
|
+
updateDocument(_id, update, upsert = false) {
|
|
25
|
+
const query = { _id };
|
|
26
|
+
const options = {
|
|
27
|
+
returnOriginal: false,
|
|
28
|
+
upsert,
|
|
29
|
+
};
|
|
30
|
+
return this._collection
|
|
31
|
+
.findOneAndUpdate(query, update, options)
|
|
32
|
+
.then((result) => result.value);
|
|
33
|
+
},
|
|
34
|
+
getDocument(_id) {
|
|
35
|
+
const query = { _id };
|
|
36
|
+
return this._collection.findOne(query);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
exports.default = appData;
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUNBLHFFQUE2QztBQUc3QyxNQUFNLDJCQUEyQixHQUFHLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxDQUFDO0FBRW5ELE1BQU0sT0FBTyxHQUFHO0lBRVosSUFBSSxXQUFXO1FBQ1gsT0FBTyxJQUFBLHVCQUFhLEVBQUMsU0FBUyxDQUFDLENBQUM7SUFDcEMsQ0FBQztJQUVELFVBQVUsRUFBRSxDQUFDO0lBRWIsb0JBQW9CLENBQUUsUUFBc0I7UUFDeEMsT0FBTyxJQUFJLENBQUMsV0FBVzthQUNsQixTQUFTLENBQUMsUUFBZSxDQUFDO2FBQzFCLEtBQUssQ0FBQyxDQUFDLEdBQVEsRUFBRSxFQUFFLENBQUMsQ0FDakIsSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDNUIsSUFBSSxDQUFDLDJCQUEyQixDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUU7Z0JBQ2pELE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFFWixPQUFPO2FBQ1Y7WUFDRCxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDbEIsQ0FBQyxDQUFDLENBQ0wsQ0FBQyxDQUFDO0lBQ1gsQ0FBQztJQUVELGNBQWMsQ0FBRSxHQUFRLEVBQUUsTUFBYyxFQUFFLFNBQWtCLEtBQUs7UUFDN0QsTUFBTSxLQUFLLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQztRQUN0QixNQUFNLE9BQU8sR0FBRztZQUNaLGNBQWMsRUFBRSxLQUFLO1lBQ3JCLE1BQU07U0FDVCxDQUFDO1FBQ0YsT0FBTyxJQUFJLENBQUMsV0FBVzthQUNsQixnQkFBZ0IsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQzthQUN4QyxJQUFJLENBQUMsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN4QyxDQUFDO0lBRUQsV0FBVyxDQUFFLEdBQVE7UUFDakIsTUFBTSxLQUFLLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQztRQUN0QixPQUFPLElBQUksQ0FBQyxXQUFXLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQzNDLENBQUM7Q0FFSixDQUFDO0FBRUYsa0JBQWUsT0FBTyxDQUFDIn0=
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InsertOneResult, WithId } from 'mongodb';
|
|
2
|
+
import getCollection from '../getCollection';
|
|
3
|
+
import { Key } from '../mongoClient';
|
|
4
|
+
|
|
5
|
+
const mongoDuplicateKeyErrorCodes = [11000, 11001];
|
|
6
|
+
|
|
7
|
+
const appData = {
|
|
8
|
+
|
|
9
|
+
get _collection () {
|
|
10
|
+
return getCollection('appData');
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
_failReads: 0,
|
|
14
|
+
|
|
15
|
+
ensureDocumentExists (document: { _id: Key }): Promise<Document | InsertOneResult | null> {
|
|
16
|
+
return this._collection
|
|
17
|
+
.insertOne(document as any)
|
|
18
|
+
.catch((err: any) => (
|
|
19
|
+
new Promise((resolve, reject) => {
|
|
20
|
+
if (!mongoDuplicateKeyErrorCodes.includes(err.code)) {
|
|
21
|
+
reject(err);
|
|
22
|
+
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
resolve(null);
|
|
26
|
+
})
|
|
27
|
+
));
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
updateDocument (_id: Key, update: Object, upsert: boolean = false): Promise<WithId<any| null>> {
|
|
31
|
+
const query = { _id };
|
|
32
|
+
const options = {
|
|
33
|
+
returnOriginal: false,
|
|
34
|
+
upsert,
|
|
35
|
+
};
|
|
36
|
+
return this._collection
|
|
37
|
+
.findOneAndUpdate(query, update, options)
|
|
38
|
+
.then((result) => result.value);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
getDocument (_id: Key): Promise<WithId<any| null>> {
|
|
42
|
+
const query = { _id };
|
|
43
|
+
return this._collection.findOne(query);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default appData;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const errors_1 = require("@storyous/errors");
|
|
4
|
+
const attemptRecursive = async (task, startAttemptsDelay, latestTime, callNumber) => (task().catch(async (err) => {
|
|
5
|
+
if (errors_1.AppError.isConcurrentRequestError(err)) {
|
|
6
|
+
const nextCallDelay = startAttemptsDelay * (callNumber ** 2);
|
|
7
|
+
if ((Date.now() + nextCallDelay) < latestTime) {
|
|
8
|
+
await new Promise((resolve) => { setTimeout(resolve, nextCallDelay); });
|
|
9
|
+
return attemptRecursive(task, startAttemptsDelay, latestTime, ++callNumber);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
throw err;
|
|
13
|
+
}));
|
|
14
|
+
/**
|
|
15
|
+
* noLaterThan = In what time by now should be the latest attempt executed
|
|
16
|
+
* startAttemptsDelay = Starting attempts delay
|
|
17
|
+
*/
|
|
18
|
+
const concurrentTask = async (task, { noLaterThan = 2000, startAttemptsDelay = 50 } = {}) => (attemptRecursive(task, startAttemptsDelay, Date.now() + noLaterThan, 1));
|
|
19
|
+
exports.default = concurrentTask;
|
|
20
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uY3VycmVudFRhc2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjb25jdXJyZW50VGFzay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLDZDQUE0QztBQUU1QyxNQUFNLGdCQUFnQixHQUFHLEtBQUssRUFDMUIsSUFBc0IsRUFDdEIsa0JBQTBCLEVBQzFCLFVBQWtCLEVBQ2xCLFVBQWtCLEVBQ04sRUFBRSxDQUFDLENBQ2YsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLEtBQUssRUFBRSxHQUFVLEVBQUUsRUFBRTtJQUM5QixJQUFJLGlCQUFRLENBQUMsd0JBQXdCLENBQUMsR0FBRyxDQUFDLEVBQUU7UUFDeEMsTUFBTSxhQUFhLEdBQUcsa0JBQWtCLEdBQUcsQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDN0QsSUFBSSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxhQUFhLENBQUMsR0FBRyxVQUFVLEVBQUU7WUFDM0MsTUFBTSxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFLEdBQUcsVUFBVSxDQUFDLE9BQU8sRUFBRSxhQUFhLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ3hFLE9BQU8sZ0JBQWdCLENBQUMsSUFBSSxFQUFFLGtCQUFrQixFQUFFLFVBQVUsRUFBRSxFQUFFLFVBQVUsQ0FBQyxDQUFDO1NBQy9FO0tBQ0o7SUFFRCxNQUFNLEdBQUcsQ0FBQztBQUNkLENBQUMsQ0FBQyxDQUNMLENBQUM7QUFFRjs7O0dBR0c7QUFDSCxNQUFNLGNBQWMsR0FBRyxLQUFLLEVBQU0sSUFBc0IsRUFBRSxFQUFFLFdBQVcsR0FBRyxJQUFJLEVBQUUsa0JBQWtCLEdBQUcsRUFBRSxFQUFFLEdBQUcsRUFBRSxFQUFFLEVBQUUsQ0FBQyxDQUMvRyxnQkFBZ0IsQ0FBQyxJQUFJLEVBQUUsa0JBQWtCLEVBQUUsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLFdBQVcsRUFBRSxDQUFDLENBQUMsQ0FDMUUsQ0FBQztBQUVGLGtCQUFlLGNBQWMsQ0FBQyJ9
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AppError } from '@storyous/errors';
|
|
2
|
+
|
|
3
|
+
const attemptRecursive = async <T> (
|
|
4
|
+
task: () => Promise<T>,
|
|
5
|
+
startAttemptsDelay: number,
|
|
6
|
+
latestTime: number,
|
|
7
|
+
callNumber: number,
|
|
8
|
+
): Promise<any> => (
|
|
9
|
+
task().catch(async (err: Error) => {
|
|
10
|
+
if (AppError.isConcurrentRequestError(err)) {
|
|
11
|
+
const nextCallDelay = startAttemptsDelay * (callNumber ** 2);
|
|
12
|
+
if ((Date.now() + nextCallDelay) < latestTime) {
|
|
13
|
+
await new Promise((resolve) => { setTimeout(resolve, nextCallDelay); });
|
|
14
|
+
return attemptRecursive(task, startAttemptsDelay, latestTime, ++callNumber);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw err;
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* noLaterThan = In what time by now should be the latest attempt executed
|
|
24
|
+
* startAttemptsDelay = Starting attempts delay
|
|
25
|
+
*/
|
|
26
|
+
const concurrentTask = async <T> (task: () => Promise<T>, { noLaterThan = 2000, startAttemptsDelay = 50 } = {}) => (
|
|
27
|
+
attemptRecursive(task, startAttemptsDelay, Date.now() + noLaterThan, 1)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export default concurrentTask;
|
package/lib/fixtures.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const lodash_1 = require("lodash");
|
|
4
|
+
const fixtures = {
|
|
5
|
+
ensureInCollection(_fixtures, collection) {
|
|
6
|
+
const fixtureIds = [];
|
|
7
|
+
const objectMap = {};
|
|
8
|
+
_fixtures.forEach((object) => {
|
|
9
|
+
fixtureIds.push(object._id);
|
|
10
|
+
objectMap[object._id.toString()] = object;
|
|
11
|
+
});
|
|
12
|
+
return collection.find({ _id: { $in: fixtureIds } })
|
|
13
|
+
.toArray()
|
|
14
|
+
.then((existing) => {
|
|
15
|
+
existing.forEach((object) => {
|
|
16
|
+
const stringId = object._id.toString();
|
|
17
|
+
delete objectMap[stringId];
|
|
18
|
+
});
|
|
19
|
+
const filteredObjects = (0, lodash_1.toArray)(objectMap);
|
|
20
|
+
if (filteredObjects.length) {
|
|
21
|
+
return collection.insertMany(filteredObjects);
|
|
22
|
+
}
|
|
23
|
+
return Promise.resolve(null);
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
exports.default = fixtures;
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZml4dHVyZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJmaXh0dXJlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLG1DQUFpQztBQUdqQyxNQUFNLFFBQVEsR0FBRztJQUNiLGtCQUFrQixDQUFFLFNBQXFCLEVBQUUsVUFBc0I7UUFDN0QsTUFBTSxVQUFVLEdBQWUsRUFBRSxDQUFDO1FBQ2xDLE1BQU0sU0FBUyxHQUE2QixFQUFFLENBQUM7UUFFL0MsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFO1lBQ3pCLFVBQVUsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQzVCLFNBQVMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxDQUFDLEdBQUcsTUFBTSxDQUFDO1FBQzlDLENBQUMsQ0FBQyxDQUFDO1FBRUgsT0FBTyxVQUFVLENBQUMsSUFBSSxDQUNsQixFQUFFLEdBQUcsRUFBRSxFQUFFLEdBQUcsRUFBRSxVQUFVLEVBQUUsRUFBRSxDQUMvQjthQUNJLE9BQU8sRUFBRTthQUNULElBQUksQ0FDRCxDQUFDLFFBQVEsRUFBRSxFQUFFO1lBQ1QsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFO2dCQUN4QixNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxDQUFDO2dCQUN2QyxPQUFPLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUMvQixDQUFDLENBQUMsQ0FBQztZQUVILE1BQU0sZUFBZSxHQUFlLElBQUEsZ0JBQU8sRUFBQyxTQUFTLENBQUMsQ0FBQztZQUV2RCxJQUFJLGVBQWUsQ0FBQyxNQUFNLEVBQUU7Z0JBQ3hCLE9BQU8sVUFBVSxDQUFDLFVBQVUsQ0FBQyxlQUFlLENBQUMsQ0FBQzthQUNqRDtZQUVELE9BQU8sT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNqQyxDQUFDLENBQUMsQ0FBQztJQUNmLENBQUM7Q0FDSixDQUFDO0FBRUYsa0JBQWUsUUFBUSxDQUFDIn0=
|
package/lib/fixtures.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { toArray } from 'lodash';
|
|
2
|
+
import { Collection, Document, ObjectId } from 'mongodb';
|
|
3
|
+
|
|
4
|
+
const fixtures = {
|
|
5
|
+
ensureInCollection (_fixtures: Document[], collection: Collection): Promise<any> {
|
|
6
|
+
const fixtureIds: ObjectId[] = [];
|
|
7
|
+
const objectMap: Record<string, Document> = {};
|
|
8
|
+
|
|
9
|
+
_fixtures.forEach((object) => {
|
|
10
|
+
fixtureIds.push(object._id);
|
|
11
|
+
objectMap[object._id.toString()] = object;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return collection.find(
|
|
15
|
+
{ _id: { $in: fixtureIds } },
|
|
16
|
+
)
|
|
17
|
+
.toArray()
|
|
18
|
+
.then(
|
|
19
|
+
(existing) => {
|
|
20
|
+
existing.forEach((object) => {
|
|
21
|
+
const stringId = object._id.toString();
|
|
22
|
+
delete objectMap[stringId];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const filteredObjects: Document[] = toArray(objectMap);
|
|
26
|
+
|
|
27
|
+
if (filteredObjects.length) {
|
|
28
|
+
return collection.insertMany(filteredObjects);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Promise.resolve(null);
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default fixtures;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const mongoClient_1 = __importDefault(require("./mongoClient"));
|
|
7
|
+
exports.default = (name) => mongoClient_1.default.getClient().db().collection(name);
|
|
8
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2V0Q29sbGVjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImdldENvbGxlY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFDQSxnRUFBd0M7QUFFeEMsa0JBQWUsQ0FBSSxJQUFZLEVBQWlCLEVBQUUsQ0FBQyxxQkFBVyxDQUFDLFNBQVMsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const errors_1 = require("@storyous/errors");
|
|
7
|
+
const mongodb_1 = require("mongodb");
|
|
8
|
+
const concurrentTask_1 = __importDefault(require("./concurrentTask"));
|
|
9
|
+
const mongoErrorCodes_1 = __importDefault(require("./mongoErrorCodes"));
|
|
10
|
+
const _removeDeprecatedIndex = async (collection) => {
|
|
11
|
+
const collectionStats = await collection.stats();
|
|
12
|
+
if (collectionStats.indexDetails.acquiredAt_1) {
|
|
13
|
+
await collection.dropIndex('acquiredAt_1');
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
exports.default = async (collection) => {
|
|
17
|
+
await _removeDeprecatedIndex(collection);
|
|
18
|
+
await collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
19
|
+
return async (key, callback, { noLaterThan = 1000, startAttemptsDelay = 50, expireIn = 120000 } = {}) => {
|
|
20
|
+
const lockId = new mongodb_1.ObjectId();
|
|
21
|
+
const releaseLock = () => collection.deleteOne({ _id: key, lockId });
|
|
22
|
+
const acquireLock = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const now = new Date();
|
|
25
|
+
/**
|
|
26
|
+
* 3 possible situations
|
|
27
|
+
* a) There is no lock -> upsert will create the document
|
|
28
|
+
* b) There is an expired lock -> expiresAt: { $lte: now } will match and the document will be replaced
|
|
29
|
+
* c) There is an unexpired lock -> expiresAt: { $lte: now } will not match,
|
|
30
|
+
* database will try to insert the new one, but it will fail on duplicate key (_id) error
|
|
31
|
+
*/
|
|
32
|
+
await collection.replaceOne({ _id: key, expiresAt: { $lte: now } },
|
|
33
|
+
// replaceOne will use the _id from filter parameter when inserting new document
|
|
34
|
+
{ lockId, expiresAt: new Date(now.getTime() + expireIn) }, { upsert: true });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (mongoErrorCodes_1.default.DUPLICATE_KEY.includes(err.code)) {
|
|
38
|
+
throw errors_1.AppError.concurrentRequest('The lock is already acquired.');
|
|
39
|
+
}
|
|
40
|
+
await releaseLock(); // the lock could be possible acquired
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
await (0, concurrentTask_1.default)(acquireLock, { noLaterThan, startAttemptsDelay });
|
|
45
|
+
try {
|
|
46
|
+
return await callback();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await releaseLock();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2V0TW9uZ29Mb2NrZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnZXRNb25nb0xvY2tlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLDZDQUE0QztBQUM1QyxxQ0FBK0M7QUFDL0Msc0VBQThDO0FBRTlDLHdFQUFnRDtBQVVoRCxNQUFNLHNCQUFzQixHQUFHLEtBQUssRUFBRSxVQUFzQixFQUFFLEVBQUU7SUFDNUQsTUFBTSxlQUFlLEdBQUcsTUFBTSxVQUFVLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDakQsSUFBSSxlQUFlLENBQUMsWUFBWSxDQUFDLFlBQVksRUFBRTtRQUMzQyxNQUFNLFVBQVUsQ0FBQyxTQUFTLENBQUMsY0FBYyxDQUFDLENBQUM7S0FDOUM7QUFDTCxDQUFDLENBQUM7QUFFRixrQkFBZSxLQUFLLEVBQUUsVUFBc0IsRUFBRSxFQUFFO0lBRTVDLE1BQU0sc0JBQXNCLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDekMsTUFBTSxVQUFVLENBQUMsV0FBVyxDQUFDLEVBQUUsU0FBUyxFQUFFLENBQUMsRUFBRSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUUxRSxPQUFPLEtBQUssRUFDUixHQUFRLEVBQ1IsUUFBeUIsRUFDekIsRUFBRSxXQUFXLEdBQUcsSUFBSSxFQUFFLGtCQUFrQixHQUFHLEVBQUUsRUFBRSxRQUFRLEdBQUcsTUFBTSxLQUFvQixFQUFFLEVBQzVFLEVBQUU7UUFFWixNQUFNLE1BQU0sR0FBRyxJQUFJLGtCQUFRLEVBQUUsQ0FBQztRQUU5QixNQUFNLFdBQVcsR0FBRyxHQUFHLEVBQUUsQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLEVBQUUsR0FBRyxFQUFFLEdBQVUsRUFBRSxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBRTVFLE1BQU0sV0FBVyxHQUFHLEtBQUssSUFBSSxFQUFFO1lBQzNCLElBQUk7Z0JBQ0EsTUFBTSxHQUFHLEdBQUcsSUFBSSxJQUFJLEVBQUUsQ0FBQztnQkFDdkI7Ozs7OzttQkFNRztnQkFDSCxNQUFNLFVBQVUsQ0FBQyxVQUFVLENBQ3ZCLEVBQUUsR0FBRyxFQUFFLEdBQVUsRUFBRSxTQUFTLEVBQUUsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLEVBQUU7Z0JBQzdDLGdGQUFnRjtnQkFDaEYsRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLElBQUksSUFBSSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsR0FBRyxRQUFRLENBQUMsRUFBRSxFQUN6RCxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FDbkIsQ0FBQzthQUNMO1lBQUMsT0FBTyxHQUFPLEVBQUU7Z0JBQ2QsSUFBSSx5QkFBZSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFO29CQUNsRCxNQUFNLGlCQUFRLENBQUMsaUJBQWlCLENBQUMsK0JBQStCLENBQUMsQ0FBQztpQkFDckU7Z0JBQ0QsTUFBTSxXQUFXLEVBQUUsQ0FBQyxDQUFDLHNDQUFzQztnQkFDM0QsTUFBTSxHQUFHLENBQUM7YUFDYjtRQUNMLENBQUMsQ0FBQztRQUVGLE1BQU0sSUFBQSx3QkFBYyxFQUFDLFdBQVcsRUFBRSxFQUFFLFdBQVcsRUFBRSxrQkFBa0IsRUFBRSxDQUFDLENBQUM7UUFFdkUsSUFBSTtZQUNBLE9BQU8sTUFBTSxRQUFRLEVBQUUsQ0FBQztTQUUzQjtnQkFBUztZQUNOLE1BQU0sV0FBVyxFQUFFLENBQUM7U0FDdkI7SUFDTCxDQUFDLENBQUM7QUFDTixDQUFDLENBQUMifQ==
|