mongoose-blockchain
Version:
Introducing a simple block chain to mongoose
261 lines (256 loc) • 8.03 kB
JavaScript
const SHA256 = require('crypto-js/sha256')
const mongoose = require('mongoose')
var blockchainLedgerSchema
var BlockchainLedger
var cacheLedger = {}
exports.initialize = function (connection) {
try {
BlockchainLedger = connection.model('BlockchainLedger')
} catch (ex) {
if (ex.name === 'MissingSchemaError') {
blockchainLedgerSchema = new mongoose.Schema({
model: { type: String, required: true },
field: { type: String, required: true },
fieldData: { type: String },
timestamp: {
type: Date,
default: Date.now()
},
lastHash: { type: String, required: true }
})
blockchainLedgerSchema.index({ field: 1, fieldData: 1, lastHash: 1, model: 1 })
BlockchainLedger = connection.model('BlockchainLedger', blockchainLedgerSchema)
} else { throw ex }
}
}
exports.plugin = function (schema, options) {
if (!blockchainLedgerSchema || !BlockchainLedger) throw new Error('mongoose-blockchain has not been initialized')
let fields = {}
let settings = {
model: null,
field: '_id',
mining: 1,
nonce: 0,
initHashText: 'StarterBlockMakeThisASetting'
}
setSettings(options)
schema.add({
hash: {
type: String
},
timestamp: {
type: Date,
default: Date.now()
},
previousHash: {
type: String,
default: initHash()
}
})
schema.pre('insertMany', function (next) {
console.log('Blockchain does not support InsertMany')
next()
})
schema.pre('save', function (next) {
var doc = this
if (doc.isNew) {
let search = { model: settings.model, field: settings.field }
if (settings.field !== '_id')search.fieldData = doc[settings.field]
BlockchainLedger.findOne(search).then(function (updateLedger) {
if (cacheLedger[doc[settings.field]] || cacheLedger[settings.model]) {
let cache = cacheLedger[doc[settings.field]] || cacheLedger[settings.model]
doc.previousHash = cache.lastHash
doc.hash = calculateHash(doc._doc)
cache.lastHash = doc.hash
// Used timeout to to give the save enough time to catch up before we try to update it
setTimeout(updateLedgerCache, 150)
// Found that setting a small timeout allowed to seperate when you try to create more than 3+ documents at the same time
setTimeout(next, 50)
} else if (!updateLedger) {
doc.hash = calculateHash(doc._doc)
search.lastHash = doc.hash
let newBlockchainLedger = new BlockchainLedger(search)
let cacheId
if (settings.field !== '_id') {
cacheId = newBlockchainLedger.fieldData
cacheLedger[newBlockchainLedger.fieldData] = newBlockchainLedger
} else {
cacheId = settings.model
cacheLedger[settings.model] = newBlockchainLedger
}
setTimeout(function (id) {
delete cacheLedger[cacheId]
}, 5000)
newBlockchainLedger.save(function () {
next()
})
} else {
doc.previousHash = updateLedger.lastHash
doc.hash = calculateHash(doc._doc)
updateLedger.lastHash = doc.hash
updateLedger.save(function () {
next()
})
}
function updateLedgerCache () {
BlockchainLedger.findOne(search)
.then(function (_updateLedger) {
if (!_updateLedger) {
setTimeout(updateLedgerCache, 50)
} else {
_updateLedger.lastHash = doc.hash
_updateLedger.save(function (err) {
if (err) {
console.log(err, '_updateLedgerSave')
}
})
}
})
.catch(err => {
if (err) {
console.log(err, 'BlockchainLedger')
}
})
}
}).catch(err => {
if (err) {
return next(err)
}
})
} else {
next()
}
})
function validateChain (data, cb) {
if (data.length < 2) {
return cb(null, true)
}
for (let i = 1; i < data.length; i++) {
const currentBlock = data[i]
const previousBlock = data[i - 1]
if (currentBlock.hash !== calculateHash(currentBlock._doc)) {
cb(null, false)
}
if (currentBlock.previousHash !== previousBlock.hash) {
cb(null, false)
}
}
return cb(null, true)
}
function checkBlockchain (params, cb) {
let search = {}
let limit = 0
let sort = 'timestamp'
if (typeof params === 'function') cb = params
else {
if (params.find)search = params.find
if (params.limit)search = params.limit > 1 ? params.limit : 0
if (params.sort)search = params.sort
}
mongoose.model(settings.model).find(search).limit(limit).sort(sort).then(function (data) {
if (settings.field !== '_id') {
let fieldGroupings = {}
for (let i = 0; i < data.length; i++) {
let id = data[i][settings.field]
if (!fieldGroupings[id])fieldGroupings[id] = [data[i]]
else fieldGroupings[id].push(data[i])
}
let error = null
let validated = true
let keys = Object.keys(fieldGroupings)
for (let k = 0; k < keys.length; k++) {
validateChain(fieldGroupings[keys[k]], function (err, valid) {
if (err) error = err
if (!valid)validated = valid
})
}
cb(error, validated)
} else {
validateChain(data, function (err, valid) {
cb(err, valid)
})
}
}).catch(function (err) {
cb(err, false)
})
}
function calculateHash (obj) {
let data = {}
Object.keys(schema.tree).forEach(function (n) {
if (n === '__v') return
if (n === 'hash') return
if (n === 'id') return
data[n] = obj[n]
})
return SHA256(data.timestamp + JSON.stringify(data)).toString()
}
function initHash (initHashText) {
return (SHA256(Date.now() + (initHashText || settings.initHashText)).toString())
}
function getFieldLedger (doc, cb) {
let search = { model: settings.model, field: settings.field }
if (settings.field !== '_id')search.fieldData = doc[settings.field]
BlockchainLedger.findOne(search).then(function (ledger) {
cb(null, ledger)
}).catch(err => {
cb(err)
})
}
function getSettings (cb) {
if (typeof cb === 'function') {
cb(settings)
} else {
return settings
}
}
function setSettings (options, cb) {
fields = {}
switch (typeof (options)) {
case 'string':
settings.model = options
break
case 'object':
settings = Object.assign({}, settings, options)
break
}
if (settings.model == null) throw new Error('model must be set')
fields[settings.field] = {
type: String,
require: true
}
if (settings.field !== '_id') {
blockchainLedgerSchema.add(fields)
}
if (typeof cb === 'function') {
cb(settings)
} else {
return settings
}
}
function clearCache (id) {
if (id) {
delete cacheLedger[id]
} else {
cacheLedger = {}
}
}
function getCache () {
return cacheLedger
}
schema.method('getSettings', getSettings)
schema.static('getSettings', getSettings)
schema.method('setSettings', setSettings)
schema.static('setSettings', setSettings)
schema.method('checkBlockchain', checkBlockchain)
schema.static('checkBlockchain', checkBlockchain)
schema.method('calculateHash', calculateHash)
schema.static('calculateHash', calculateHash)
schema.method('initHash', initHash)
schema.static('initHash', initHash)
schema.method('getFieldLedger', getFieldLedger)
schema.static('getFieldLedger', getFieldLedger)
schema.method('getCache', getCache)
schema.static('getCache', getCache)
schema.method('clearCache', clearCache)
schema.static('clearCache', clearCache)
}