Comparing version 6.2.0 to 6.3.0
@@ -23,2 +23,15 @@ "use strict"; | ||
}; | ||
function fromDistinctToAccounts(distinctResult) { | ||
const accountsSet = new Set(); | ||
for (const fullAccountName of distinctResult) { | ||
const paths = fullAccountName.split(":"); | ||
let path = paths[0]; | ||
accountsSet.add(path); | ||
for (let i = 1; i < paths.length; ++i) { | ||
path += ":" + paths[i]; | ||
accountsSet.add(path); | ||
} | ||
} | ||
return Array.from(accountsSet); | ||
} | ||
class Book { | ||
@@ -66,3 +79,3 @@ constructor(name, options = {}) { | ||
accountForBalanceSnapshot = query.account ? [].concat(query.account).join() : undefined; | ||
balanceSnapshot = await (0, balance_1.getBestSnapshot)({ | ||
balanceSnapshot = await (0, balance_1.getBestBalanceSnapshot)({ | ||
book: parsedQuery.book, | ||
@@ -105,5 +118,5 @@ account: accountForBalanceSnapshot, | ||
// There is a snapshot already. But let's check if it's too old. | ||
const tooOld = Date.now() > balanceSnapshot.createdAt.getTime() + this.balanceSnapshotSec * 1000; | ||
const isSnapshotObsolete = Date.now() > balanceSnapshot.createdAt.getTime() + this.balanceSnapshotSec * 1000; | ||
// If it's too old we would need to cache another snapshot. | ||
if (tooOld) { | ||
if (isSnapshotObsolete) { | ||
delete parsedQuery._id; | ||
@@ -238,13 +251,69 @@ const match = { $match: parsedQuery }; | ||
async listAccounts(options = {}) { | ||
const results = await transaction_1.transactionModel.collection.distinct("accounts", { book: this.name }, { session: options.session }); | ||
const uniqueAccounts = new Set(); | ||
for (const result of results) { | ||
const prev = []; | ||
const paths = result.split(":"); | ||
for (const acct of paths) { | ||
prev.push(acct); | ||
uniqueAccounts.add(prev.join(":")); | ||
// If there is a session, we must NOT set any readPreference (as per mongo v5 and v6). | ||
// https://www.mongodb.com/docs/v6.0/core/transactions/#read-concern-write-concern-read-preference | ||
// Otherwise, we are free to use any readPreference. | ||
if (options && !options.session && !options.readPreference) { | ||
// Let's try reading from the secondary node, if available. | ||
options.readPreference = "secondaryPreferred"; | ||
} | ||
const query = { book: this.name }; | ||
let listAccountsSnapshot = null; | ||
if (this.balanceSnapshotSec) { | ||
listAccountsSnapshot = await (0, balance_1.getBestListAccountsSnapshot)({ book: this.name }); | ||
if (listAccountsSnapshot) { | ||
// Use cached balance | ||
query.timestamp = { $gte: listAccountsSnapshot.createdAt }; | ||
} | ||
} | ||
return Array.from(uniqueAccounts); | ||
const createdAt = new Date(); // take date earlier | ||
const results = (await transaction_1.transactionModel.collection.distinct("accounts", query, { | ||
session: options.session, | ||
})); | ||
let uniqueAccounts = fromDistinctToAccounts(results); | ||
if (listAccountsSnapshot) { | ||
uniqueAccounts = Array.from(new Set([...uniqueAccounts, ...listAccountsSnapshot.meta.accounts])); | ||
} | ||
if (uniqueAccounts.length === 0) | ||
return uniqueAccounts; | ||
if (this.balanceSnapshotSec) { | ||
// It's the first (ever?) snapshot for this listAccounts. We just need to save whatever we've just aggregated | ||
// so that the very next listAccounts query would use cached snapshot. | ||
if (!listAccountsSnapshot) { | ||
await (0, balance_1.snapshotListAccounts)({ | ||
book: this.name, | ||
accounts: uniqueAccounts, | ||
createdAt, | ||
expireInSec: this.expireBalanceSnapshotSec, | ||
}); | ||
} | ||
else { | ||
// There is a snapshot already. But let's check if it's too old. | ||
const isSnapshotObsolete = Date.now() > listAccountsSnapshot.createdAt.getTime() + this.balanceSnapshotSec * 1000; | ||
// If it's too old we would need to cache another snapshot. | ||
if (isSnapshotObsolete) { | ||
delete query.timestamp; | ||
// Important! We are going to recalculate the entire listAccounts from the day one. | ||
// Since this operation can take seconds (if you have millions of documents) | ||
// we better run this query IN THE BACKGROUND. | ||
// If this exact balance query would be executed multiple times at the same second we might end up with | ||
// multiple snapshots in the database. Which is fine. The chance of this happening is low. | ||
// Our main goal here is not to delay this .listAccounts() method call. The tradeoff is that | ||
// database will use 100% CPU for few (milli)seconds, which is fine. It's all fine (C) | ||
transaction_1.transactionModel.collection | ||
.distinct("accounts", query, { | ||
session: options.session, | ||
}) | ||
.then((results) => (0, balance_1.snapshotListAccounts)({ | ||
book: this.name, | ||
accounts: fromDistinctToAccounts(results), | ||
createdAt, | ||
expireInSec: this.expireBalanceSnapshotSec, | ||
})) | ||
.catch((error) => { | ||
console.error("medici: Couldn't do background listAccounts snapshot.", error); | ||
}); | ||
} | ||
} | ||
} | ||
return uniqueAccounts; | ||
} | ||
@@ -251,0 +320,0 @@ } |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getBestSnapshot = exports.snapshotBalance = exports.constructKey = exports.hashKey = exports.setBalanceSchema = exports.balanceModel = void 0; | ||
exports.getBestListAccountsSnapshot = exports.snapshotListAccounts = exports.getBestBalanceSnapshot = exports.snapshotBalance = exports.constructKey = exports.hashKey = exports.setBalanceSchema = exports.balanceModel = void 0; | ||
const crypto_1 = require("crypto"); | ||
@@ -71,3 +71,3 @@ const mongoose_1 = require("mongoose"); | ||
exports.snapshotBalance = snapshotBalance; | ||
function getBestSnapshot(query, options = {}) { | ||
function getBestBalanceSnapshot(query, options = {}) { | ||
const { book, account, meta, ...extras } = query; | ||
@@ -77,2 +77,26 @@ const key = hashKey(constructKey(book, account, { ...meta, ...extras })); | ||
} | ||
exports.getBestSnapshot = getBestSnapshot; | ||
exports.getBestBalanceSnapshot = getBestBalanceSnapshot; | ||
async function snapshotListAccounts(balanceData, options = {}) { | ||
const rawKey = "@listAccounts:" + balanceData.book; | ||
const key = hashKey(rawKey); | ||
const balanceDoc = { | ||
key, | ||
rawKey, | ||
book: balanceData.book, | ||
meta: { accounts: balanceData.accounts }, | ||
createdAt: balanceData.createdAt, | ||
expireAt: new Date(balanceData.createdAt.getTime() + balanceData.expireInSec * 1000), | ||
}; | ||
const result = await exports.balanceModel.collection.insertOne(balanceDoc, { | ||
session: options.session, | ||
writeConcern: options.session ? undefined : { w: 1, j: true }, | ||
forceServerObjectId: true, | ||
}); | ||
return result.acknowledged; | ||
} | ||
exports.snapshotListAccounts = snapshotListAccounts; | ||
async function getBestListAccountsSnapshot(query, options = {}) { | ||
const key = hashKey("@listAccounts:" + query.book); | ||
return (await exports.balanceModel.collection.findOne({ key }, { sort: { _id: -1 }, ...options })); | ||
} | ||
exports.getBestListAccountsSnapshot = getBestListAccountsSnapshot; |
{ | ||
"name": "medici", | ||
"version": "6.2.0", | ||
"version": "6.3.0", | ||
"description": "Double-entry accounting ledger for Node + Mongoose", | ||
@@ -9,2 +9,4 @@ "main": "build/index.js", | ||
"ci": "npm run build && npm run test", | ||
"ci-mongoose8": "npm i mongoose@8 && npm run test:code", | ||
"ci-mongoose7": "npm i mongoose@7 && npm run test:code", | ||
"ci-mongoose6": "npm i mongoose@6 && npm run test:code", | ||
@@ -53,3 +55,3 @@ "ci-mongoose5": "npm i mongoose@5 && npm run test:code", | ||
"dependencies": { | ||
"mongoose": "5 - 7" | ||
"mongoose": "5 - 8" | ||
}, | ||
@@ -60,2 +62,3 @@ "homepage": "https://github.com/flash-oss/medici", | ||
"@types/chai": "^4.3.5", | ||
"@types/luxon": "^3.3.5", | ||
"@types/mocha": "^10.0.1", | ||
@@ -62,0 +65,0 @@ "@types/node": "^18.16.17", |
@@ -405,2 +405,16 @@ # medici | ||
### 6.3 | ||
- The `book.listAccounts()` method is now cached same way the `book.balance()` is cached. | ||
- Add `mongoose` v8 support. | ||
### 6.2 | ||
- Add `mongoose` v7 support. | ||
- Add Node 20 support. | ||
### 6.1 | ||
- Add MongoDB v6 support. | ||
### 6.0 | ||
@@ -413,3 +427,3 @@ | ||
- The balances cache primary key is now a SHA1 hash of the previous value. Before: `"MyBook;Account;clientId.$in.0:12345,clientId.$in.1:67890,currency:USD"`. After: `"\u001b\u0004NÞj\u0013rÅ\u001b¼,F_#\u001cÔk Nv"`. Allows each key to be exactly 40 bytes (20 chars) regadless the actual balance query text length. | ||
- The balances cache primary key is now a SHA1 hash of the previous value. Before: `"MyBook;Account;clientId.$in.0:12345,clientId.$in.1:67890,currency:USD"`. After: `"\u001b\u0004NÞj\u0013rÅ\u001b¼,F_#\u001cÔk Nv"`. Allows each key to be exactly 40 bytes (20 chars) regardless the actual balance query text length. | ||
- But the old raw unhashed key is now stored in `rawKey` of `medici_balances` for DX and troubleshooting purposes. | ||
@@ -416,0 +430,0 @@ - Fixed important bugs #58 and #70 related to retrieving balance for a custom schema properties. Thanks @dolcalmi |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
84828
1265
523
27
+ Added@types/whatwg-url@11.0.5(transitive)
+ Addedbson@6.10.0(transitive)
+ Addedkareem@2.6.3(transitive)
+ Addedmongodb@6.10.0(transitive)
+ Addedmongodb-connection-string-url@3.0.1(transitive)
+ Addedmongoose@8.8.2(transitive)
+ Addedsift@17.1.3(transitive)
+ Addedtr46@4.1.1(transitive)
+ Addedwhatwg-url@13.0.0(transitive)
- Removed@types/node@22.9.3(transitive)
- Removed@types/whatwg-url@8.2.2(transitive)
- Removedbson@5.5.1(transitive)
- Removedip-address@9.0.5(transitive)
- Removedjsbn@1.1.0(transitive)
- Removedkareem@2.5.1(transitive)
- Removedmongodb@5.9.2(transitive)
- Removedmongodb-connection-string-url@2.6.0(transitive)
- Removedmongoose@7.8.2(transitive)
- Removedsift@16.0.1(transitive)
- Removedsmart-buffer@4.2.0(transitive)
- Removedsocks@2.8.3(transitive)
- Removedsprintf-js@1.1.3(transitive)
- Removedtr46@3.0.0(transitive)
- Removedundici-types@6.19.8(transitive)
- Removedwhatwg-url@11.0.0(transitive)
Updatedmongoose@5 - 8