Skip to content

Commit

Permalink
Possibility to restore a removed document (#8)
Browse files Browse the repository at this point in the history
* Possibility to restore a removed document

* (Soft) removing a document creates a empty version as the latest
  version (possible to list using force).
* Added possibility to specify correlation id when removing (added to
  the empty version specified above).
* Added possibility to restore an entity to a specific version. This
  will create a copy of the version and put it as the latest version.
  This will also remove the soft remove of the entity.
* Changed the behaviour of the remove function, the cb only takes an
  err, not a res. The error will be raised if no such entity exists.
* The changed behaviour is a breaking API change and thus the major
  version has been bumped.
* Removed check that entity is not already removed when removing
  • Loading branch information
markusn authored Feb 2, 2018
1 parent e0a2286 commit f6dbc2f
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 51 deletions.
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Purpose and features
Opionated solution for storing documents as entities in postgres
without an ORM-solution. This package aims to use postgres as a
document store (storing json data as documents) but with the benefits
document store (storing JSON data as documents) but with the benefits
of Postgres as a platform and with the occasional use of SQL features
such as referential integrity between identifiers.

Expand Down Expand Up @@ -41,22 +41,41 @@ db.load(id, (dbErr, entity) => {

### (Soft) remove a document
```js
db.remove(id, (dbErr, res) => {
db.remove(id, (dbErr) => {
if (dbErr) return dbErr;
// res.removed will contain the id if there was an entity to remove
// this will mark the entity as removed and add an empty version as
// the last version
});

```

### Restore a version of a document
```js
db.restore(versionId, (dbErr, entity) => {
if (dbErr) return dbErr;
// this will make versionId the last version of the entity to which
// it belongs and unmark the entity as removed (as there is an
// explicit call to restore the version)
});

```

## Versions
All updates uses upsert and stores updates to an existing entity as a
new version in the entity\_version table. The entity table contains a
reference to the latest version. Attributes are stored in the
entity\_version table, i.e. the attributes column contains a specific
reference to the latest version. The document is stored in the
entity\_version table, i.e. the doc column contains a specific
version of the JSON document.

## (Soft) removal
A document can be marked as removed. A document and its' versions will
no longer be listed of read using the load and list functions, unless
the second argument is set to true (force). It is not possible to
upsert a removed document (it will be considered a conflict).
upsert a removed document (it will be considered a conflict). Removing
a document adds an empty document as the latest version of the entity
(for traceability).

It is possible to un-remove a document by restoring it to a specific
version, this will create a new (latest) version of the document using
the data from the specified version. This also marks the document as
not removed.
65 changes: 58 additions & 7 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,64 @@ function loadByExternalId(entityType, systemName, externalIdType, id, cb) {
});
}

function remove(id, cb) {
const q = ["UPDATE entity",
"SET entity_removed = now()",
"WHERE entity_id = $1 AND entity_removed IS NULL"];
pgClient.query(q.join(" "), [id], (err, res) => {
function remove(id, correlationId, cb) {
if (typeof correlationId === "function") {
cb = correlationId;
correlationId = null;
}

load(id, (err, doc) => {
if (err) return cb(err);
if (!doc) return cb(new Error("no such entity"));

const emptyDoc = {
id: id,
type: doc.type,
meta: {correlationId: correlationId}
};

return upsert(emptyDoc, (upsertErr) => {
if (upsertErr) return cb(upsertErr);
const q = [
"UPDATE entity",
"SET entity_removed = now()",
"WHERE entity_id = $1"];
return pgClient.query(q.join(" "), [id], (rmErr, res) => {
if (rmErr) return cb(rmErr);
if (res.rowCount === 0) return cb(new Error("Could not remove"));
return cb(null);
});
});
});
}

function restoreVersion(versionId, correlationId, cb) {
if (typeof correlationId === "function") {
cb = correlationId;
correlationId = null;
}

loadVersion(versionId, true, (err, res) => {
if (err) return cb(err);

const entity = res.entity;
entity.meta.correlationId = correlationId;

return maybeUnRemove(entity.id, (unremoveErr) => {
if (unremoveErr) return cb(unremoveErr);
return upsert(entity, cb);
});
});
}

function maybeUnRemove(id, cb) {
const q = [
"UPDATE entity",
"SET entity_removed = null",
"WHERE entity_id = $1"];
pgClient.query(q.join(" "), [id], (err) => {
if (err) return cb(err);
if (res.rowCount === 0) return cb(null, {removed: null});
return cb(null, {removed: id});
return cb(null);
});
}

Expand Down Expand Up @@ -217,6 +267,7 @@ module.exports = {
loadVersion,
listVersions,
remove,
restoreVersion,
upsert,
tables
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"Linus Thiel",
"Markus Ekholm"
],
"version": "1.4.1",
"version": "2.0.0",
"scripts": {
"pretest": "docker-compose build",
"test": "node_modules/.bin/mocha",
Expand Down
50 changes: 32 additions & 18 deletions test/feature/entity-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Feature("Entity", () => {

Scenario("Save, remove and forcefully load an entity", () => {
let savedEntity;
const rmCorrId = "z";

before((done) => {
helper.clearAndInit(done);
Expand All @@ -98,7 +99,7 @@ Feature("Entity", () => {
});

When("we delete it", (done) => {
query.remove(entity.id, (err) => {
query.remove(entity.id, rmCorrId, (err) => {
if (err) return done(err);
return done();
});
Expand All @@ -112,10 +113,11 @@ Feature("Entity", () => {
});
});

And("it should have the same data", () => {
And("it should have no data except the id, type and correlation id", () => {
savedEntity.id.should.equal(entity.id);
savedEntity.type.should.equal(entity.type);
savedEntity.attributes.name.should.equal(entity.attributes.name);
savedEntity.meta.correlationId.should.equal(rmCorrId);
should.equal(savedEntity.attributes, undefined);
});
});

Expand Down Expand Up @@ -146,6 +148,9 @@ Feature("Entity", () => {
});

Scenario("Removing an entity that has been soft removed", () => {

let gotErr;

before((done) => {
helper.clearAndInit(done);
});
Expand All @@ -155,37 +160,45 @@ Feature("Entity", () => {
});

And("we remove the entity", (done) => {
query.remove(entity.id, (err, res) => {
query.remove(entity.id, (err) => {
if (err) return done(err);
should.equal(res.removed, entity.id);
return done();
});
});

When("we try to remove it again nothing is removed", (done) => {
query.remove(entity.id, (err, res) => {
if (err) return done(err);
should.equal(res.removed, null);
return done();
When("we try to remove it again", (done) => {
query.remove(entity.id, (err) => {
gotErr = err;
done();
});
});

Then("we get an error", () => {
should.not.equal(gotErr, null);
});

});

Scenario("Removing an entity that does not exist.", () => {

let gotErr;

before((done) => {
helper.clearAndInit(done);
});

When("We remove an entity that never existed we should have removed nothing.", (done) => {
query.remove(entity.id, (err, res) => {
if (err) return done(err);
should.equal(res.removed, null);
return done();
When("we remove an entity that never existed", (done) => {
query.remove(entity.id, (err) => {
gotErr = err;
done();
});
});
});

Then("we should get an error", () => {
should.not.equal(gotErr, null);
});

});

Scenario("Saving an entity without a type should yield error", () => {
let upsertErr = null;
Expand Down Expand Up @@ -243,7 +256,7 @@ Feature("Entity", () => {
Given("that there TWO entities in the db", (done) => {
query.upsert(entity, (err) => {
if (err) return done(err);
query.upsert(otherEntity, done);
return query.upsert(otherEntity, done);
});
});

Expand Down Expand Up @@ -296,11 +309,12 @@ Feature("Entity", () => {
Given("that there are two entities in the db with the same externalId", (done) => {
query.upsert(entity, (err) => {
if (err) return done(err);
query.upsert(otherEntity, done);
return query.upsert(otherEntity, done);
});
});

let error, savedEntity;

When("we try to load ONE of them by externalId", (done) => {
query.loadByExternalId(entity.type, "system", "type", "externalId", (err, dbEntity) => {
error = err;
Expand Down
Loading

0 comments on commit f6dbc2f

Please sign in to comment.