Skip to content

Commit

Permalink
Add ability to persist History lines with partial entities
Browse files Browse the repository at this point in the history
  • Loading branch information
ddolcimascolo committed Nov 10, 2024
1 parent 02a305b commit ee236aa
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 14 deletions.
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,28 @@ whereas the options are listed here (with default value).

```js
{
/* runs the insert within the sequelize hook chain, disable
for increased performance without warranties */
blocking: true,
/* By default sequelize-temporal persist only changes, and saves the previous state in the history table.
The "full" option saves all transactions into the temporal database
(i.e. this includes the latest state.)
This allows to only query the hostory table to get the full history of an entity.
*/
full: false
/**
* Runs the insert within the sequelize hook chain, disable for increased performance without warranties
*/
blocking: true,
/**
* By default sequelize-temporal persist only changes, and saves the previous state in the history table.
* The "full" option saves all transactions into the temporal database (i.e. this includes the latest state.)
* This allows to only query the hostory table to get the full history of an entity.
*/
full: false,
/**
* Whether to skip inserting an History line if the update() call is silent.
*/
skipIfSilent: false,
/**
* This library will sometimes reload() an updated entity because it misses a few attributes. In order to minimize
* the number of extra SQL queries, an heuristic is applied to perform the reload() only if necessary. This mainly
* boils down to "is this attribute <x> missing ?".
* The `reloadIgnoredAttributes` can be used to ignore certain attributes from this check.
*/
reloadIgnoredAttributes: ['deletedAt']
}
```

Details
Expand Down
33 changes: 29 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ var temporalDefaultOptions = {
// for increased performance
blocking: true,
full: false,
skipIfSilent: false
skipIfSilent: false,
reloadIgnoredAttributes: ['deletedAt']
};

var excludeAttributes = function(obj, attrsToExclude){
Expand Down Expand Up @@ -36,8 +37,9 @@ var Temporal = function(model, sequelize, temporalOptions){
}
};

var attributes = model.getAttributes();
var excludedAttributes = ["Model","unique","primaryKey","autoIncrement", "set", "get", "_modelAttribute"];
var historyAttributes = _(model.rawAttributes).mapValues(function(v){
var historyAttributes = _(attributes).mapValues(function(v){
v = excludeAttributes(v, excludedAttributes);
// remove the "NOW" defaultValue for the default timestamps
// we want to save them, but just a copy from our master record
Expand Down Expand Up @@ -70,8 +72,31 @@ var Temporal = function(model, sequelize, temporalOptions){
return;
}

var dataValues = (!temporalOptions.full && obj._previousDataValues) || obj.dataValues;
var historyRecord = modelHistory.create(dataValues, {transaction: options.transaction});
async function getDataValues() {
if (!temporalOptions.full) {
return obj._previousDataValues || obj.dataValues;
}

const attributesToReload = Object.keys(attributes).filter(attribute => {
if (!temporalOptions.reloadIgnoredAttributes.includes(attribute) && !(attribute in obj.dataValues)) {
return true;
}
});

/**
* In 'full' mode, it's important that we are able to save a History version even though the caller initially fetched
* a partial instance. We do so at the cost of an extra SELECT to get up-to-date values (remember that in 'full' mode
* we are using afterX hooks, so this works).
* Model.reload() will do its magic to merge the newly fetched values directly in dataValues. #gg
*/
if (attributesToReload.length > 0) {
await obj.reload({transaction: options.transaction, attributes: attributesToReload, paranoid: false})
}

return obj.dataValues;
}

var historyRecord = getDataValues().then(dataValues => modelHistory.create(dataValues, {transaction: options.transaction}));

if(temporalOptions.blocking){
return historyRecord;
Expand Down
27 changes: 26 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ describe('Read-only API', function(){
// overwrites the old SQLite DB
sequelize = new Sequelize('', '', '', {
dialect: 'sqlite',
storage: __dirname + '/.test.sqlite'
storage: __dirname + '/.test.sqlite',
logging: process.env.LOGGING === 'true' ? console.log : false
});
User = Temporal(sequelize.define('User', {
name: Sequelize.TEXT
Expand Down Expand Up @@ -325,6 +326,30 @@ describe('Read-only API', function(){
.then(assertCount(UserHistory,0));
});

it('onUpdate: should store the previous version to the historyDB even if entity was partially loaded' , async function(){
const created = await User.create({ name: 'name' });
const user = await User.findByPk(created.id, { attributes: ['id', 'name'] }); // Don't fetch timestamps

await user.update({ name: 'newName' });
await user.update({ name: 'thirdName' });

const history = await UserHistory.findAll();

assert.equal(history.length, 3, 'initial revision and to updates saved');

const [initial, firstUpdate, secondUpdate] = history;

assert.equal(+initial.createdAt, +firstUpdate.createdAt, 'createdAt was saved during first update, despite not being eagerly loaded');
assert.equal(+initial.createdAt, +secondUpdate.createdAt, 'createdAt was saved during second update, despite not being eagerly loaded');

assert.isAtLeast(firstUpdate.updatedAt, initial.createdAt, 'updatedAt was saved during first update');
assert.isAtLeast(secondUpdate.updatedAt, firstUpdate.updatedAt, 'updatedAt was saved during second update');

assert.equal('name', initial.name);
assert.equal('newName', firstUpdate.name);
assert.equal('thirdName', secondUpdate.name);
});

});

describe('silent mode', function(){
Expand Down

0 comments on commit ee236aa

Please sign in to comment.