Comparing version 5.0.0-next.0 to 5.0.0-next.1
@@ -5,37 +5,51 @@ "use strict"; | ||
const Entry_1 = require("./Entry"); | ||
const parseQuery_1 = require("./helper/parseQuery"); | ||
const journals_1 = require("./models/journals"); | ||
const transactions_1 = require("./models/transactions"); | ||
const parseFilterQuery_1 = require("./helper/parse/parseFilterQuery"); | ||
const parseBalanceQuery_1 = require("./helper/parse/parseBalanceQuery"); | ||
const journal_1 = require("./models/journal"); | ||
const transaction_1 = require("./models/transaction"); | ||
const JournalNotFoundError_1 = require("./errors/JournalNotFoundError"); | ||
const BookConstructorError_1 = require("./errors/BookConstructorError"); | ||
const locks_1 = require("./models/locks"); | ||
const lock_1 = require("./models/lock"); | ||
const balance_1 = require("./models/balance"); | ||
class Book { | ||
constructor(name, options = {}) { | ||
this.name = name; | ||
this.precision = | ||
typeof options.precision !== "undefined" ? options.precision : 7; | ||
this.maxAccountPath = | ||
typeof options.maxAccountPath !== "undefined" | ||
? options.maxAccountPath | ||
: 3; | ||
this.precision = options.precision != null ? options.precision : 8; | ||
this.maxAccountPath = options.maxAccountPath != null ? options.maxAccountPath : 3; | ||
this.balanceSnapshotSec = options.balanceSnapshotSec != null ? options.balanceSnapshotSec : 24 * 60 * 60; | ||
if (typeof this.name !== "string" || this.name.trim().length === 0) { | ||
throw new BookConstructorError_1.BookConstructorError("Invalid value for name provided."); | ||
} | ||
if (typeof this.precision !== "number" || | ||
!Number.isInteger(this.precision) || | ||
this.precision < 0) { | ||
if (typeof this.precision !== "number" || !Number.isInteger(this.precision) || this.precision < 0) { | ||
throw new BookConstructorError_1.BookConstructorError("Invalid value for precision provided."); | ||
} | ||
if (typeof this.maxAccountPath !== "number" || | ||
!Number.isInteger(this.maxAccountPath) || | ||
this.maxAccountPath < 0) { | ||
if (typeof this.maxAccountPath !== "number" || !Number.isInteger(this.maxAccountPath) || this.maxAccountPath < 0) { | ||
throw new BookConstructorError_1.BookConstructorError("Invalid value for maxAccountPath provided."); | ||
} | ||
if (typeof this.balanceSnapshotSec !== "number" || this.balanceSnapshotSec < 0) { | ||
throw new BookConstructorError_1.BookConstructorError("Invalid value for balanceSnapshotSec provided."); | ||
} | ||
} | ||
entry(memo, date = null, original_journal = null) { | ||
entry(memo, date = null, original_journal) { | ||
return Entry_1.Entry.write(this, memo, date, original_journal); | ||
} | ||
async balance(query, options = {}) { | ||
const parsedQuery = (0, parseBalanceQuery_1.parseBalanceQuery)(query, this); | ||
let balanceSnapshot = null; | ||
let accountForBalanceSnapshot; | ||
let needToDoBalanceSnapshot = true; | ||
if (this.balanceSnapshotSec) { | ||
accountForBalanceSnapshot = query.account ? [].concat(query.account).join() : undefined; | ||
balanceSnapshot = await (0, balance_1.getBestSnapshot)({ | ||
book: parsedQuery.book, | ||
account: accountForBalanceSnapshot, | ||
meta: parsedQuery.meta, | ||
}, options); | ||
if (balanceSnapshot) { | ||
parsedQuery._id = { $gt: balanceSnapshot.transaction }; | ||
needToDoBalanceSnapshot = Date.now() > balanceSnapshot.createdAt.getTime() + this.balanceSnapshotSec * 1000; | ||
} | ||
} | ||
const match = { | ||
$match: (0, parseQuery_1.parseQuery)(query, this), | ||
$match: parsedQuery, | ||
}; | ||
@@ -45,63 +59,68 @@ const group = { | ||
_id: null, | ||
balance: { | ||
$sum: { | ||
$subtract: ["$credit", "$debit"], | ||
}, | ||
}, | ||
count: { | ||
$sum: 1, | ||
}, | ||
balance: { $sum: { $subtract: ["$credit", "$debit"] } }, | ||
count: { $sum: 1 }, | ||
lastTransactionId: { $last: "$_id" }, | ||
lastTimestamp: { $last: "$timestamp" }, | ||
}, | ||
}; | ||
const result = (await transactions_1.transactionModel.aggregate([match, group], options).exec())[0]; | ||
return !result | ||
? { | ||
balance: 0, | ||
notes: 0, | ||
const result = (await transaction_1.transactionModel.collection.aggregate([match, group], options).toArray())[0]; | ||
let balance = 0; | ||
let notes = 0; | ||
if (balanceSnapshot) { | ||
balance += balanceSnapshot.balance; | ||
} | ||
if (result) { | ||
balance += parseFloat(result.balance.toFixed(this.precision)); | ||
notes = result.count; | ||
if (needToDoBalanceSnapshot && result.lastTransactionId) { | ||
await (0, balance_1.snapshotBalance)({ | ||
book: this.name, | ||
account: accountForBalanceSnapshot, | ||
meta: parsedQuery.meta, | ||
transaction: result.lastTransactionId, | ||
timestamp: result.lastTimestamp, | ||
balance, | ||
expireInSec: this.balanceSnapshotSec * 2, // Keep the document twice longer than needed in case this particular balance() query is not executed very often. | ||
}, options); | ||
} | ||
: { | ||
balance: parseFloat(result.balance.toFixed(this.precision)), | ||
notes: result.count, | ||
}; | ||
} | ||
return { balance, notes }; | ||
} | ||
async ledger(query, populate = null, options = {}) { | ||
let skip; | ||
let limit = 0; | ||
const { lean = true } = options; | ||
async ledger(query, options = {}) { | ||
// Pagination | ||
if (query.perPage) { | ||
skip = (query.page ? query.page - 1 : 0) * query.perPage; | ||
limit = query.perPage; | ||
const { perPage, page, ...restOfQuery } = query; | ||
const paginationOptions = {}; | ||
if (Number.isSafeInteger(perPage)) { | ||
paginationOptions.skip = (Number.isSafeInteger(page) ? page - 1 : 0) * perPage; | ||
paginationOptions.limit = perPage; | ||
} | ||
const filterQuery = (0, parseQuery_1.parseQuery)(query, this); | ||
const q = transactions_1.transactionModel | ||
.find(filterQuery, undefined, options) | ||
.lean(lean) | ||
.sort({ | ||
datetime: -1, | ||
timestamp: -1, | ||
}); | ||
let count = Promise.resolve(0); | ||
if (typeof skip !== "undefined") { | ||
count = transactions_1.transactionModel | ||
.countDocuments(filterQuery) | ||
.session(options.session || null) | ||
.exec(); | ||
q.skip(skip).limit(limit); | ||
const filterQuery = (0, parseFilterQuery_1.parseFilterQuery)(restOfQuery, this); | ||
const findPromise = transaction_1.transactionModel.collection | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
.find(filterQuery, { | ||
...paginationOptions, | ||
sort: { | ||
datetime: -1, | ||
timestamp: -1, | ||
}, | ||
session: options.session, | ||
}) | ||
.toArray(); | ||
let countPromise = Promise.resolve(0); | ||
if (paginationOptions.limit) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
countPromise = transaction_1.transactionModel.collection.countDocuments(filterQuery, { session: options.session }); | ||
} | ||
if (populate) { | ||
for (const p of populate) { | ||
if ((0, transactions_1.isValidTransactionKey)(p)) { | ||
q.populate(p); | ||
} | ||
} | ||
} | ||
const results = (await q.exec()); | ||
const results = await findPromise; | ||
return { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
results, | ||
total: (await count) || results.length, | ||
total: (await countPromise) || results.length, | ||
}; | ||
} | ||
async void(journal_id, reason, options = {}) { | ||
const journal = await journals_1.journalModel | ||
const journal = await journal_1.journalModel | ||
.findOne({ | ||
@@ -123,3 +142,3 @@ _id: journal_id, | ||
for (const account of accounts) { | ||
await locks_1.lockModel.collection.updateOne({ account, book: this.name }, { | ||
await lock_1.lockModel.collection.updateOne({ account, book: this.name }, { | ||
$set: { updatedAt: new Date() }, | ||
@@ -133,3 +152,3 @@ $setOnInsert: { book: this.name, account }, | ||
async listAccounts(options = {}) { | ||
const results = await transactions_1.transactionModel | ||
const results = await transaction_1.transactionModel | ||
.find({ book: this.name }, undefined, options) | ||
@@ -136,0 +155,0 @@ .lean(true) |
@@ -5,7 +5,8 @@ "use strict"; | ||
const mongoose_1 = require("mongoose"); | ||
const transactions_1 = require("./models/transactions"); | ||
const transaction_1 = require("./models/transaction"); | ||
const TransactionError_1 = require("./errors/TransactionError"); | ||
const journals_1 = require("./models/journals"); | ||
const journal_1 = require("./models/journal"); | ||
const isPrototypeAttribute_1 = require("./helper/isPrototypeAttribute"); | ||
const InvalidAccountPathLengthError_1 = require("./errors/InvalidAccountPathLengthError"); | ||
const parseDateField_1 = require("./helper/parse/parseDateField"); | ||
class Entry { | ||
@@ -15,17 +16,11 @@ constructor(book, memo, date, original_journal) { | ||
this.book = book; | ||
this.journal = new journals_1.journalModel(); | ||
this.journal.memo = memo; | ||
this.journal = new journal_1.journalModel(); | ||
this.journal.memo = String(memo); | ||
if (original_journal) { | ||
this.journal._original_journal = | ||
typeof original_journal === "string" | ||
? new mongoose_1.Types.ObjectId(original_journal) | ||
: original_journal; | ||
typeof original_journal === "string" ? new mongoose_1.Types.ObjectId(original_journal) : original_journal; | ||
} | ||
if (!date) { | ||
date = new Date(); | ||
} | ||
this.journal.datetime = date; | ||
this.journal.datetime = (0, parseDateField_1.parseDateField)(date) || new Date(); | ||
this.journal.book = this.book.name; | ||
this.transactions = []; | ||
this.journal.approved = true; | ||
} | ||
@@ -35,6 +30,2 @@ static write(book, memo, date, original_journal) { | ||
} | ||
setApproved(value) { | ||
this.journal.approved = value; | ||
return this; | ||
} | ||
transact(type, account_path, amount, extra) { | ||
@@ -51,8 +42,6 @@ if (typeof account_path === "string") { | ||
const transaction = { | ||
_id: new mongoose_1.Types.ObjectId(), | ||
// _id: keys are generated on the database side for better consistency | ||
_journal: this.journal._id, | ||
_original_journal: this.journal._original_journal, | ||
account_path, | ||
accounts: account_path.join(":"), | ||
approved: true, | ||
book: this.book.name, | ||
@@ -63,7 +52,7 @@ credit, | ||
memo: this.journal.memo, | ||
meta: {}, | ||
timestamp: new Date(), | ||
void_reason: undefined, | ||
voided: false, | ||
}; | ||
if (this.journal._original_journal) { | ||
transaction._original_journal = this.journal._original_journal; | ||
} | ||
if (extra) { | ||
@@ -73,6 +62,8 @@ for (const [key, value] of Object.entries(extra)) { | ||
continue; | ||
if ((0, transactions_1.isValidTransactionKey)(key)) { | ||
if ((0, transaction_1.isValidTransactionKey)(key)) { | ||
transaction[key] = value; | ||
} | ||
else { | ||
if (!transaction.meta) | ||
transaction.meta = {}; | ||
transaction.meta[key] = value; | ||
@@ -83,3 +74,2 @@ } | ||
this.transactions.push(transaction); | ||
this.journal._transactions.push(transaction._id); | ||
return this; | ||
@@ -96,4 +86,2 @@ } | ||
for (const tx of this.transactions) { | ||
// set approved on transactions to approved-value on journal | ||
tx.approved = this.journal.approved; | ||
// sum the value of the transaction | ||
@@ -107,4 +95,21 @@ total += tx.credit - tx.debit; | ||
try { | ||
const result = await transaction_1.transactionModel.collection.insertMany(this.transactions, { | ||
forceServerObjectId: true, | ||
ordered: true, | ||
session: options.session, | ||
writeConcern: options.session ? undefined : { w: 1, j: true }, // Ensure at least ONE node wrote to JOURNAL (disk) | ||
}); | ||
let insertedIds = Object.values(result.insertedIds); | ||
if (insertedIds.length !== this.transactions.length) { | ||
throw new TransactionError_1.TransactionError(`Saved only ${insertedIds.length} of ${this.transactions.length} transactions`, total); | ||
} | ||
if (!insertedIds[0]) { | ||
// Mongo returns `undefined` as the insertedIds when forceServerObjectId=true. Let's re-read it. | ||
const txs = await transaction_1.transactionModel.collection | ||
.find({ _journal: this.transactions[0]._journal }, { projection: { _id: 1 }, session: options.session }) | ||
.toArray(); | ||
insertedIds = txs.map((tx) => tx._id); | ||
} | ||
this.journal._transactions = insertedIds; | ||
await this.journal.save(options); | ||
await Promise.all(this.transactions.map((tx) => new transactions_1.transactionModel(tx).save(options))); | ||
if (options.writelockAccounts && options.session) { | ||
@@ -124,12 +129,2 @@ const writelockAccounts = options.writelockAccounts instanceof RegExp | ||
if (!options.session) { | ||
try { | ||
await transactions_1.transactionModel | ||
.deleteMany({ | ||
_journal: this.journal._id, | ||
}) | ||
.exec(); | ||
} | ||
catch (e) { | ||
console.error(`Can't delete txs for journal ${this.journal._id}. Medici ledger consistency got harmed.`, e); | ||
} | ||
throw new TransactionError_1.TransactionError(`Failure to save journal: ${err.message}`, total); | ||
@@ -136,0 +131,0 @@ } |
@@ -8,3 +8,3 @@ "use strict"; | ||
super(message); | ||
this.code = 403; | ||
this.code = 404; | ||
this.code = code; | ||
@@ -11,0 +11,0 @@ } |
@@ -5,3 +5,8 @@ "use strict"; | ||
class MediciError extends Error { | ||
constructor(message, code = 500) { | ||
super(message); | ||
this.code = 500; | ||
this.code = code; | ||
} | ||
} | ||
exports.MediciError = MediciError; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.initModels = void 0; | ||
const journals_1 = require("../models/journals"); | ||
const locks_1 = require("../models/locks"); | ||
const transactions_1 = require("../models/transactions"); | ||
const journal_1 = require("../models/journal"); | ||
const transaction_1 = require("../models/transaction"); | ||
const lock_1 = require("../models/lock"); | ||
const balance_1 = require("../models/balance"); | ||
async function initModels() { | ||
await journals_1.journalModel.init(); | ||
await transactions_1.transactionModel.init(); | ||
await locks_1.lockModel.init(); | ||
await journal_1.journalModel.init(); | ||
await transaction_1.transactionModel.init(); | ||
await lock_1.lockModel.init(); | ||
await balance_1.balanceModel.init(); | ||
} | ||
exports.initModels = initModels; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.syncIndexes = void 0; | ||
const journals_1 = require("../models/journals"); | ||
const locks_1 = require("../models/locks"); | ||
const transactions_1 = require("../models/transactions"); | ||
const journal_1 = require("../models/journal"); | ||
const lock_1 = require("../models/lock"); | ||
const transaction_1 = require("../models/transaction"); | ||
async function syncIndexes(options) { | ||
await journals_1.journalModel.syncIndexes(options); | ||
await transactions_1.transactionModel.syncIndexes(options); | ||
await locks_1.lockModel.syncIndexes(options); | ||
await journal_1.journalModel.syncIndexes(options); | ||
await transaction_1.transactionModel.syncIndexes(options); | ||
await lock_1.lockModel.syncIndexes(options); | ||
} | ||
exports.syncIndexes = syncIndexes; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Book = exports.TransactionError = exports.JournalNotFoundError = exports.JournalAlreadyVoidedError = exports.InvalidAccountPathLengthError = exports.BookConstructorError = exports.MediciError = exports.syncIndexes = exports.initModels = exports.mongoTransaction = exports.setLockSchema = exports.setTransactionSchema = exports.setJournalSchema = void 0; | ||
exports.Book = exports.TransactionError = exports.JournalNotFoundError = exports.JournalAlreadyVoidedError = exports.InvalidAccountPathLengthError = exports.BookConstructorError = exports.MediciError = exports.syncIndexes = exports.initModels = exports.mongoTransaction = exports.setBalanceSchema = exports.setLockSchema = exports.setTransactionSchema = exports.setJournalSchema = void 0; | ||
const Book_1 = require("./Book"); | ||
Object.defineProperty(exports, "Book", { enumerable: true, get: function () { return Book_1.Book; } }); | ||
var journals_1 = require("./models/journals"); | ||
Object.defineProperty(exports, "setJournalSchema", { enumerable: true, get: function () { return journals_1.setJournalSchema; } }); | ||
var transactions_1 = require("./models/transactions"); | ||
Object.defineProperty(exports, "setTransactionSchema", { enumerable: true, get: function () { return transactions_1.setTransactionSchema; } }); | ||
var locks_1 = require("./models/locks"); | ||
Object.defineProperty(exports, "setLockSchema", { enumerable: true, get: function () { return locks_1.setLockSchema; } }); | ||
var journal_1 = require("./models/journal"); | ||
Object.defineProperty(exports, "setJournalSchema", { enumerable: true, get: function () { return journal_1.setJournalSchema; } }); | ||
var transaction_1 = require("./models/transaction"); | ||
Object.defineProperty(exports, "setTransactionSchema", { enumerable: true, get: function () { return transaction_1.setTransactionSchema; } }); | ||
var lock_1 = require("./models/lock"); | ||
Object.defineProperty(exports, "setLockSchema", { enumerable: true, get: function () { return lock_1.setLockSchema; } }); | ||
var balance_1 = require("./models/balance"); | ||
Object.defineProperty(exports, "setBalanceSchema", { enumerable: true, get: function () { return balance_1.setBalanceSchema; } }); | ||
var mongoTransaction_1 = require("./helper/mongoTransaction"); | ||
@@ -13,0 +15,0 @@ Object.defineProperty(exports, "mongoTransaction", { enumerable: true, get: function () { return mongoTransaction_1.mongoTransaction; } }); |
{ | ||
"name": "medici", | ||
"version": "5.0.0-next.0", | ||
"version": "5.0.0-next.1", | ||
"description": "Simple double-entry accounting for Node + Mongoose", | ||
@@ -9,2 +9,3 @@ "main": "build/index.js", | ||
"ci": "npm run lint && npm run build && npm run test", | ||
"ci-mongoose5": "npm i mongoose@5 && npm run test:code", | ||
"bench:balance": "ts-node ./bench/bench-balance.ts", | ||
@@ -22,3 +23,3 @@ "bench:ledger": "ts-node ./bench/bench-ledger.ts", | ||
"test:types": "tsd", | ||
"test:coverage": "npm run clean && nyc ts-mocha --recursive './spec/**/*.spec.ts'" | ||
"test:coverage": "npm run clean && USE_MEMORY_REPL_SET=true nyc --reporter html ts-mocha --recursive './spec/**/*.spec.ts'" | ||
}, | ||
@@ -51,3 +52,3 @@ "files": [ | ||
"dependencies": { | ||
"mongoose": "^6.1.3" | ||
"mongoose": "5 - 6" | ||
}, | ||
@@ -54,0 +55,0 @@ "homepage": "https://github.com/flash-oss/medici", |
205
README.md
@@ -33,9 +33,9 @@ # medici | ||
* You can safely add values up to 9007199254740991 (Number.MAX_SAFE_INTEGER) and by default down to 0.000001 (precision: 7). | ||
* Anything more than 9007199254740991 or less than 0.000001 (precision: 7) is not guaranteed to be handled properly. | ||
- You can safely add values up to 9007199254740991 (Number.MAX_SAFE_INTEGER) and by default down to 0.00000001 (precision: 8). | ||
- Anything more than 9007199254740991 or less than 0.00000001 (precision: 8) is not guaranteed to be handled properly. | ||
You can set the floating point precision as follows: | ||
You can set the floating point precision as follows: | ||
```javascript | ||
const myBook = new Book("MyBook", { precision: 8 }); | ||
const myBook = new Book("MyBook", { precision: 7 }); | ||
``` | ||
@@ -91,7 +91,9 @@ | ||
const { results, total } = await myBook.ledger({ | ||
account: "Income", | ||
start_date: startDate, | ||
end_date: endDate, | ||
}, null, { lean: true }); | ||
const { results, total } = await myBook.ledger( | ||
{ | ||
account: "Income", | ||
start_date: startDate, | ||
end_date: endDate, | ||
}, | ||
); | ||
``` | ||
@@ -121,36 +123,35 @@ | ||
async function withdraw(walletId: string, amount: number) { | ||
return mongoTransaction(async session => { | ||
return mongoTransaction(async (session) => { | ||
await mainLedger | ||
.entry("Withdraw by User") | ||
.credit("Assets", amount) | ||
.debit(`Accounts:${walletId}`, amount) | ||
.commit({ session }); | ||
await mainLedger | ||
.entry("Withdraw by User") | ||
.credit("Assets", amount) | ||
.debit(`Accounts:${walletId}`, amount) | ||
.commit({ session }); | ||
// .balance() can be a resource-expensive operation. So we do it after we | ||
// created the journal. | ||
const balanceAfter = await mainLedger.balance( | ||
{ | ||
account: `Accounts:${walletId}`, | ||
}, | ||
{ session } | ||
); | ||
// .balance() can be a resource-expensive operation. So we do it after we | ||
// created the journal. | ||
const balanceAfter = await mainLedger.balance( | ||
{ | ||
account: `Accounts:${walletId}`, | ||
}, | ||
{ session } | ||
); | ||
// Avoid spending more than the wallet has. | ||
// Reject the ACID transaction by throwing this exception. | ||
if (balanceAfter.balance < 0) { | ||
throw new Error("Not enough balance in wallet."); | ||
} | ||
// Avoid spending more than the wallet has. | ||
// Reject the ACID transaction by throwing this exception. | ||
if (balanceAfter.balance < 0) { | ||
throw new Error("Not enough balance in wallet."); | ||
} | ||
// ISBN: 978-1-4842-6879-7. MongoDB Performance Tuning (2021), p. 217 | ||
// Reduce the Chance of Transient Transaction Errors by moving the | ||
// contentious statement to the end of the transaction. | ||
// ISBN: 978-1-4842-6879-7. MongoDB Performance Tuning (2021), p. 217 | ||
// Reduce the Chance of Transient Transaction Errors by moving the | ||
// contentious statement to the end of the transaction. | ||
// We writelock only the account of the User/Wallet. If we writelock a very | ||
// often used account, like the fictitious Assets account in this example, | ||
// we would slow down the database extremely as the writelocks would make | ||
// it impossible to concurrently write in the database. | ||
// We only check the balance of the User/Wallet, so only this Account has to | ||
// be writelocked. | ||
await mainLedger.writelockAccounts([`Accounts:${walletId}`], { session }); | ||
// We writelock only the account of the User/Wallet. If we writelock a very | ||
// often used account, like the fictitious Assets account in this example, | ||
// we would slow down the database extremely as the writelocks would make | ||
// it impossible to concurrently write in the database. | ||
// We only check the balance of the User/Wallet, so only this Account has to | ||
// be writelocked. | ||
await mainLedger.writelockAccounts([`Accounts:${walletId}`], { session }); | ||
}); | ||
@@ -183,6 +184,2 @@ } | ||
void_reason: String, | ||
approved: { | ||
type: Boolean, | ||
default: true, | ||
}, | ||
}; | ||
@@ -215,6 +212,2 @@ ``` | ||
_original_journal: Schema.Types.ObjectId, | ||
approved: { | ||
type: Boolean, | ||
default: true, | ||
}, | ||
}; | ||
@@ -227,3 +220,3 @@ ``` | ||
If you need to have related documents for Transactions and want to use Mongoose's `populate` method, or if you need to add additional fields to the schema that the `meta` won't satisfy, you can define your own schema for `Medici_Transaction` and use the `setJournalSchema` and `setTransactionSchema` to use those schemas. When you specify meta values when querying or writing transactions, the system will check the Transaction schema to see if those values correspond to actual top-level fields, and if so will set those instead of the corresponding `meta` field. | ||
If you need to add additional fields to the schema that the `meta` won't satisfy, you can define your own schema for `Medici_Transaction` and utilise the `setJournalSchema` and `setTransactionSchema` to use those schemas. When you specify meta values when querying or writing transactions, the system will check the Transaction schema to see if those values correspond to actual top-level fields, and if so will set those instead of the corresponding `meta` field. | ||
@@ -256,6 +249,2 @@ For example, if you want transactions to have a related "person" document, you can define the transaction schema like so and use setTransactionSchema to register it: | ||
void_reason: String, | ||
approved: { | ||
type: Boolean, | ||
default: true, | ||
}, | ||
}; | ||
@@ -273,4 +262,2 @@ | ||
Then when you query transactions using the `book.ledger()` method, you can specify the related documents to populate as the second argument. E.g., `book.ledger({account:'Assets:Accounts Receivable'}, ['_person']).then()...` | ||
## Performance | ||
@@ -287,3 +274,2 @@ | ||
"book": 1, | ||
"approved": 1, | ||
"datetime": -1, | ||
@@ -296,3 +282,2 @@ "timestamp": -1 | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -304,3 +289,2 @@ | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -313,3 +297,2 @@ | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -329,3 +312,2 @@ | ||
"book": 1, | ||
"approved": 1, | ||
"datetime": -1, | ||
@@ -341,3 +323,2 @@ "timestamp": -1 | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -352,3 +333,2 @@ | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -364,3 +344,2 @@ | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -375,3 +354,2 @@ | ||
"book": 1, | ||
"approved": 1, | ||
"datetime": -1, | ||
@@ -389,3 +367,2 @@ "timestamp": -1 | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -400,3 +377,2 @@ | ||
"book": 1, | ||
"approved": 1 | ||
``` | ||
@@ -416,50 +392,67 @@ | ||
- **v5.0.0** | ||
### v5.0.0 | ||
- The project was rewritten with TypeScript. Types are provided within the package now. | ||
- Added support for MongoDB sessions (aka ACID transactions). See `IOptions` type. | ||
- Added a `mongoTransaction`-method, which is a convenience shortcut for `mongoose.connection.transaction`. | ||
- Added async helper method `initModels`, which initializes the underlying `transactionModel` and `journalModel`. Use this after you connected to the MongoDB-Server if you want to use transactions. Or else you could get `Unable to read from a snapshot due to pending collection catalog changes; please retry the operation.`-Error when acquiring a session because the actual database-collection is still being created by the underlying mongoose-instance. | ||
- **BREAKING**: Node.js 12 is the lowest supported version. Although, 10 should still work fine, when using mongoose v5. | ||
- **BREAKING**: You can't import `book` anymore. Only `Book` is supported. `require("medici").Book`. | ||
Mongoose v6 is the only supported version now. Avoid using both v5 and v6 in the same project. | ||
- MongoDB 4 and above is supported. You can still use MongoDB 3, but ACID-sessions could have issues. | ||
- **BREAKING**: You can't import `book` anymore. Only `Book` is supported. `require("medici").Book`. | ||
- Added a new index on the transactionModel to improve the performance of paginated ledger queries. | ||
- **BREAKING**: `.ledger()` returns lean Transaction-Objects for better performance. To retrieve hydrated Transaction-Objects, set lean to false in the third parameter of `.ledger()`. It is recommended to not hydrate the transactions, as it implies that the transactions could be manipulated and the data integrity of Medici could be risked. | ||
- You can now specify the `precision`. Book now accepts an optional second parameter, where you can set the `precision` used internally by Medici. Default value is 7 digits precision. Javascript has issues with floating points precision and can only handle 16 digits precision, like 0.1 + 0.2 results in 0.30000000000000004 and not 0.3. The default precision of 7 digits after decimal, results in the correct result of 0.1 + 0.2 = 0.3. The default value is taken from medici version 4.0.2. Be careful, if you use currency, which has more decimal points, e.g. Bitcoin has a precision of 8 digits after the comma. So for Bitcoin you should set the precision to 8. You can enforce an "only-Integer"-mode, by setting the precision to 0. But keep in mind that Javascript has a max safe integer limit of 9007199254740991. | ||
- Added `maxAccountPath`. You can set the maximum amount of account paths via the second parameter of Book. This can improve the performance of `.balance()` and `.ledger()` calls as it will then use the accounts attribute of the transactions as a filter. | ||
- **BREAKING**: Added validation for `name` of Book, `maxAccountPath` and `precision`. A `name` has to be not an empty string or a string containing only whitespace characters. `precision` has to be an integer bigger or equal 0. `maxAccountPath` has to be an integer bigger or equal 0. | ||
- Added `setJournalSchema` and `setTransactionSchema` to use custom Schemas. It will ensure, that all relevant middlewares and methods are also added when using custom Schemas. Use `syncIndexes`-method from medici after setTransactionSchema to enforce the defined indexes on the models. | ||
- **BREAKING**: Added prototype-pollution protection when creating entries. Reserved words like `__proto__` can not be used as properties of a Transaction or a Journal or their meta-Field and they will get silently filtered. | ||
- **BREAKING**: When calling `book.void()` the provided `journal_id` has to belong to the `book`. If the journal does not exist within the book, medici will throw a `JournalNotFoundError`. In medici < 5 you could theoretically void a `journal` of another `book`. | ||
- Added a `trackAccountChanges`-model to make it possible to call `.balance()` and get a reliable result while using a mongo-session. | ||
- **BREAKING**: `.balance()` does not support pagination anymore. To get the balance of a page sum up the values of credit and debit of a paginated `.ledger()`-call. | ||
- Added a `lockModel` to make it possible to call `.balance()` and get a reliable result while using a mongo-session. Call `.writelockAccounts()` with first parameter being an Array of Accounts, which you want to lock. E.g. `book.writelockAccounts(["Assets:User:User1"], { session })`. For best performance call writelockAccounts as the last operation in the transaction. Also `.commit()` accepts the option `writelockAccounts`, where you can provide an array of accounts or a RegExp. It is recommended to use the `book.writelockAccounts()`. | ||
High level overview. | ||
- **v4.0.0** | ||
- The project was rewritten with **TypeScript**. Types are provided within the package now. | ||
- Added support for MongoDB sessions (aka **ACID** transactions). See `IOptions` type. | ||
- Did number of consistency, stability, server disk space, and speed improvements. | ||
- Mongoose v5 and v6 are supported now. | ||
- Node.js 8 is required now. | ||
- Drop support of Mongoose v4. Only v5 is supported now. (But v4 should just work, even though not tested.) | ||
- No API changes. | ||
Technical changes of the release. | ||
- **v3.0.0** | ||
- Added a `mongoTransaction`-method, which is a convenience shortcut for `mongoose.connection.transaction`. | ||
- Added async helper method `initModels`, which initializes the underlying `transactionModel` and `journalModel`. Use this after you connected to the MongoDB-Server if you want to use transactions. Or else you could get `Unable to read from a snapshot due to pending collection catalog changes; please retry the operation.` error when acquiring a session because the actual database-collection is still being created by the underlying mongoose-instance. | ||
- Added `setJournalSchema` and `setTransactionSchema` to use custom Schemas. It will ensure, that all relevant middlewares and methods are also added when using custom Schemas. Use `syncIndexes`-method from medici after setTransactionSchema to enforce the defined indexes on the models. | ||
- Added `maxAccountPath`. You can set the maximum amount of account paths via the second parameter of Book. This can improve the performance of `.balance()` and `.ledger()` calls as it will then use the accounts attribute of the transactions as a filter. | ||
- MongoDB v4 and above is supported. You can still try using MongoDB v3, but it's not recommended. | ||
- Added a new `timestamp+datetime` index on the transactionModel to improve the performance of paginated ledger queries. | ||
- Added a `lockModel` to make it possible to call `.balance()` and **get a reliable result while using a mongo-session**. Call `.writelockAccounts()` with first parameter being an Array of Accounts, which you want to lock. E.g. `book.writelockAccounts(["Assets:User:User1"], { session })`. For best performance call writelockAccounts as the last operation in the transaction. Also `.commit()` accepts the option `writelockAccounts`, where you can provide an array of accounts or a RegExp. It is recommended to use the `book.writelockAccounts()`. | ||
- **POTENTIALLY BREAKING**: Node.js 12 is the lowest supported version. Although, 10 should still work fine. | ||
- **POTENTIALLY BREAKING**: `.ledger()` returns lean Transaction-Objects for better performance. To retrieve hydrated mongoose models set `lean` to `false` in the third parameter of `.ledger()`. It is recommended to not hydrate the transactions, as it implies that the transactions could be manipulated and the data integrity of Medici could be risked. | ||
- **POTENTIALLY BREAKING**: Rounding precision was changed from 7 to 8 floating point digits. | ||
- The new default precision is 8 digits. The medici v4 had it 7 by default. Be careful if you are using values which have more than 8 digits after the comma. | ||
- You can now specify the `precision` in the `Book` constructor as an optional second parameter `precision`. Simulating medici v4 behaviour: `new Book("MyBook", { precision: 7 })`. | ||
- Also, you can enforce an "only-Integer"-mode, by setting the precision to 0. But keep in mind that Javascript has a max safe integer limit of 9007199254740991. | ||
- **POTENTIALLY BREAKING**: Added validation for `name` of Book, `maxAccountPath` and `precision`. | ||
- The `name` has to be not an empty string or a string containing only whitespace characters. | ||
- `precision` has to be an integer bigger or equal 0. | ||
- `maxAccountPath` has to be an integer bigger or equal 0. | ||
- **POTENTIALLY BREAKING**: Added prototype-pollution protection when creating entries. Reserved words like `__proto__` can not be used as properties of a Transaction or a Journal or their meta-Field. They will get silently filtered out. | ||
- **POTENTIALLY BREAKING**: When calling `book.void()` the provided `journal_id` has to belong to the `book`. If the journal does not exist within the book, medici will throw a `JournalNotFoundError`. In medici < 5 you could theoretically void a `journal` of another `book`. | ||
- **POTENTIALLY BREAKING**: Transaction document properties `meta`, `voided`, `void_reason`, `_original_journal` won't be stored to the database when have no data. In medici v4 they were `{}`, `false`, `null`, `null` correspondingly. | ||
- **BREAKING**: Transactions are now committed/voided using native `insertMany`/`updateMany` instead of mongoose `.save()` method. If you had any "pre save" middlewares on the `medici_transactions` they won't be working anymore. | ||
- **BREAKING**: If you had any other Mongoose middlewares installed onto medici `transactionModel` or `journalModel` then they won't work anymore. Medici v5 is not using the mongoose to do DB operations. Instead, we execute commands via bare `mongodb` driver. | ||
- **BREAKING**: `.balance()` does not support pagination anymore. To get the balance of a page sum up the values of credit and debit of a paginated `.ledger()`-call. | ||
- **BREAKING**: You can't import `book` anymore. Only `Book` is supported. `require("medici").Book`. | ||
- **BREAKING**: The approving functionality (`approved` and `setApproved()`) was removed. It's complicating code, bloating the DB, not used by anyone maintainers know. Please, implement approvals outside the ledger. If you still need it to be part of the ledger then you're out of luck and would have to (re)implement it yourself. Sorry about that. | ||
- Add 4 mandatory indexes, otherwise queries get very slow when transactions collection grows. | ||
- No API changes. | ||
### v4.0.0 | ||
- **v2.0.0** | ||
- Node.js 8 is required now. | ||
- Drop support of Mongoose v4. Only v5 is supported now. (But v4 should just work, even though not tested.) | ||
- No API changes. | ||
- Upgrade to use mongoose v5. To use with mongoose v4 just `npm i medici@1`. | ||
- Support node.js v10. | ||
- No API changes. | ||
### v3.0.0 | ||
- **v1.0.0** _See [this PR](https://github.com/flash-oss/medici/pull/5) for more details_ | ||
- **BREAKING**: Dropped support of node.js v0.10, v0.12, v4, and io.js. Node.js >= v6 is supported only. This allowed to drop several production dependencies. Also, few bugs were automatically fixed. | ||
- **BREAKING**: Upgraded `mongoose` to v4. This allows `medici` to be used with wider mongodb versions. | ||
- Dropped production dependencies: `moment`, `q`, `underscore`. | ||
- Dropped dev dependencies: `grunt`, `grunt-exec`, `grunt-contrib-coffee`, `grunt-sed`, `grunt-contrib-watch`, `semver`. | ||
- No `.coffee` any more. Using node.js v6 compatible JavaScript only. | ||
- There are no API changes. | ||
- Fixed a [bug](https://github.com/flash-oss/medici/issues/4). Transaction meta data was not voided correctly. | ||
- This module maintainer is now [flash-oss](https://github.com/flash-oss) instead of the original author [jraede](http://github.com/jraede). | ||
- Add 4 mandatory indexes, otherwise queries get very slow when transactions collection grows. | ||
- No API changes. | ||
### v2.0.0 | ||
- Upgrade to use mongoose v5. To use with mongoose v4 just `npm i medici@1`. | ||
- Support node.js v10. | ||
- No API changes. | ||
### v1.0.0 | ||
_See [this PR](https://github.com/flash-oss/medici/pull/5) for more details_ | ||
- **BREAKING**: Dropped support of node.js v0.10, v0.12, v4, and io.js. Node.js >= v6 is supported only. This allowed to drop several production dependencies. Also, few bugs were automatically fixed. | ||
- **BREAKING**: Upgraded `mongoose` to v4. This allows `medici` to be used with wider mongodb versions. | ||
- Dropped production dependencies: `moment`, `q`, `underscore`. | ||
- Dropped dev dependencies: `grunt`, `grunt-exec`, `grunt-contrib-coffee`, `grunt-sed`, `grunt-contrib-watch`, `semver`. | ||
- No `.coffee` any more. Using node.js v6 compatible JavaScript only. | ||
- There are no API changes. | ||
- Fixed a [bug](https://github.com/flash-oss/medici/issues/4). Transaction meta data was not voided correctly. | ||
- This module maintainer is now [flash-oss](https://github.com/flash-oss) instead of the original author [jraede](http://github.com/jraede). |
@@ -17,5 +17,4 @@ // Generated by dts-bundle-generator v6.2.0 | ||
book: string; | ||
voided: boolean; | ||
void_reason: string; | ||
approved: boolean; | ||
voided?: boolean; | ||
void_reason?: string; | ||
} | ||
@@ -27,6 +26,6 @@ export declare type TJournalDocument<T extends IJournal = IJournal> = Omit<Document, "__v" | "id"> & T & { | ||
export interface ITransaction { | ||
_id: Types.ObjectId; | ||
_id?: Types.ObjectId; | ||
credit: number; | ||
debit: number; | ||
meta: IAnyObject; | ||
meta?: IAnyObject; | ||
datetime: Date; | ||
@@ -39,5 +38,4 @@ account_path: string[]; | ||
timestamp: Date; | ||
voided: boolean; | ||
voided?: boolean; | ||
void_reason?: string; | ||
approved: boolean; | ||
_original_journal?: Types.ObjectId | IJournal; | ||
@@ -54,5 +52,4 @@ } | ||
transactions: U[]; | ||
static write<U extends ITransaction, J extends IJournal>(book: Book, memo: string, date: Date | null, original_journal: string | Types.ObjectId | null): Entry<U, J>; | ||
constructor(book: Book, memo: string, date: Date | null, original_journal: string | Types.ObjectId | null); | ||
setApproved(value: boolean): Entry<U, J>; | ||
static write<U extends ITransaction, J extends IJournal>(book: Book, memo: string, date: Date | null, original_journal?: string | Types.ObjectId): Entry<U, J>; | ||
constructor(book: Book, memo: string, date: Date | null, original_journal?: string | Types.ObjectId); | ||
private transact; | ||
@@ -65,3 +62,3 @@ credit<T extends IAnyObject = IAnyObject>(account_path: string | string[], amount: number | string, extra?: T & Partial<U>): Entry<U, J>; | ||
} | ||
export declare type IParseQuery = { | ||
export declare type IFilterQuery = { | ||
account?: string | string[]; | ||
@@ -71,4 +68,3 @@ _journal?: Types.ObjectId | string; | ||
end_date?: Date | string | number; | ||
approved?: boolean; | ||
} & { | ||
} & Partial<ITransaction> & { | ||
[key: string]: string[] | number | string | Date | boolean | Types.ObjectId; | ||
@@ -80,2 +76,9 @@ }; | ||
} | ||
export declare type IBalanceQuery = { | ||
account?: string | string[]; | ||
start_date?: Date | string | number; | ||
end_date?: Date | string | number; | ||
} & { | ||
[key: string]: string[] | number | string | Date | boolean | Types.ObjectId; | ||
}; | ||
export declare class Book<U extends ITransaction = ITransaction, J extends IJournal = IJournal> { | ||
@@ -85,20 +88,18 @@ name: string; | ||
maxAccountPath: number; | ||
balanceSnapshotSec: number; | ||
constructor(name: string, options?: { | ||
precision?: number; | ||
maxAccountPath?: number; | ||
balanceSnapshotSec?: number; | ||
}); | ||
entry(memo: string, date?: Date, original_journal?: string | Types.ObjectId): Entry<U, J>; | ||
balance(query: IParseQuery, options?: IOptions): Promise<{ | ||
balance(query: IBalanceQuery, options?: IOptions): Promise<{ | ||
balance: number; | ||
notes: number; | ||
}>; | ||
ledger<T = U>(query: IParseQuery & IPaginationQuery, populate?: string[] | null, options?: IOptions & { | ||
lean?: true; | ||
}): Promise<{ | ||
ledger<T = U>(query: IFilterQuery & IPaginationQuery, options?: IOptions): Promise<{ | ||
results: T[]; | ||
total: number; | ||
}>; | ||
ledger<T = U>(query: IParseQuery & IPaginationQuery, populate?: string[] | null, options?: IOptions & { | ||
lean?: false; | ||
}): Promise<{ | ||
ledger<T = U>(query: IFilterQuery & IPaginationQuery, options?: IOptions): Promise<{ | ||
results: (Document & T)[]; | ||
@@ -112,2 +113,3 @@ total: number; | ||
export declare function setLockSchema(schema: Schema, collection?: string): void; | ||
export declare function setBalanceSchema(schema: Schema, collection?: string): void; | ||
export declare function mongoTransaction<T = unknown>(fn: (session: ClientSession) => Promise<T>): Promise<any>; | ||
@@ -119,2 +121,4 @@ export declare function initModels(): Promise<void>; | ||
export declare class MediciError extends Error { | ||
code: number; | ||
constructor(message: string, code?: number); | ||
} | ||
@@ -121,0 +125,0 @@ export declare class BookConstructorError extends MediciError { |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
65005
29
1001
439
+ Added@aws-sdk/client-cognito-identity@3.696.0(transitive)
+ Added@aws-sdk/client-sso-oidc@3.696.0(transitive)
+ Added@aws-sdk/client-sts@3.696.0(transitive)
+ Added@aws-sdk/credential-provider-cognito-identity@3.696.0(transitive)
+ Added@aws-sdk/credential-provider-ini@3.696.0(transitive)
+ Added@aws-sdk/credential-provider-node@3.696.0(transitive)
+ Added@aws-sdk/credential-provider-sso@3.696.0(transitive)
+ Added@aws-sdk/credential-providers@3.696.0(transitive)
+ Added@aws-sdk/token-providers@3.696.0(transitive)
+ Added@types/node@22.9.1(transitive)
- Removed@aws-sdk/client-cognito-identity@3.699.0(transitive)
- Removed@aws-sdk/client-sso-oidc@3.699.0(transitive)
- Removed@aws-sdk/client-sts@3.699.0(transitive)
- Removed@aws-sdk/credential-provider-cognito-identity@3.699.0(transitive)
- Removed@aws-sdk/credential-provider-ini@3.699.0(transitive)
- Removed@aws-sdk/credential-provider-node@3.699.0(transitive)
- Removed@aws-sdk/credential-provider-sso@3.699.0(transitive)
- Removed@aws-sdk/credential-providers@3.699.0(transitive)
- Removed@aws-sdk/token-providers@3.699.0(transitive)
- Removed@types/node@22.9.3(transitive)
Updatedmongoose@5 - 6