Skip to content

Commit

Permalink
Merge pull request #4 from haensl/3
Browse files Browse the repository at this point in the history
#3: Add patch and diffing functions.
  • Loading branch information
haensl committed Jul 28, 2023
2 parents ebfa9c4 + 4dcc59c commit 74a686a
Show file tree
Hide file tree
Showing 6 changed files with 693 additions and 441 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
## 1.1.0
* [#3: Add patch and diffing logic.](https://github.com/haensl/mongo/issues/3)
* Update dependencies.

## 1.0.0
* [#1: Bootstrap project.](https://github.com/haensl/mongo/issues/1)
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ The `mongo` utility wraps functions around getting a [`MongoClient`](), identify
// Takes Mongo connection URI.
// Returns service object.
({ mongoUri }) => ({
// Collects the changes made to doc by the given patch.
// Returns an array of objects of the form { field, from , to }
changes: (doc, patch) => [changes],
// Cleanup function to close the connection pool.
// Invoke e.g. at instance shutdown.
cleanup: async () => void,
Expand All @@ -78,7 +82,19 @@ The `mongo` utility wraps functions around getting a [`MongoClient`](), identify
// Checks if the given Error is a MongoDB error with given code.
// Returns a boolean.
isError: (error, code) => boolean
isError: (error, code) => boolean,
// Checks if the given patch would change the given field in doc.
// Returns a boolean.
patchChangesField: ({ doc, field, patch }) => boolean,
// Returns the value stored in obj at the given keyPath.
// A key path is a string like `prop.subProp`.
resolveKeyPath: (obj, keyPath) => any,
// Transforms a patch object (with sub objects)
// into a MongoDB patch (with `prop.sub` keys)
translatePatchToMongoUpdate: (patch) => mongoPatch
})
```

Expand Down
110 changes: 109 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,118 @@ const isError = (error, code) =>
// https://mongodb.github.io/node-mongodb-native/4.4/classes/MongoError.html#code
error instanceof MongoError && error.code == code; // eslint-disable-line eqeqeq

/**
* Collects the changes made in the given patch.
*
* @param doc a document.
* @param patch a patch to apply to the document.
*
* @return an array of changes contained in the patch.
* Changes are described by objects of the form { field, from, to }.
*/
const changes = (doc, patch) => {
const mongoPatch = translatePatchToMongoUpdate(patch);
const changes = [];

for (const field of Object.keys(mongoPatch)) {
const fieldIsChanging = patchChangesField({
doc,
field,
patch
});

if (fieldIsChanging) {
changes.push({
field,
from: resolveKeyPath(doc, field),
to: mongoPatch[field]
});
}
}

return changes;
};

/**
* Checks whether the given field is changed within patch.
*
* @param doc the document to check.
* @param field the field being checked for updates. When checking
* subdocuments, this needs to be the property path, e.g. `cycle.length`.
* @param patch the patch to check
*
* @return Promise a promise that resolves to a boolean indicating
* whether or not the patch changes the field.
*/
const patchChangesField = ({ doc, field, patch }) => {
const mongoPatch = translatePatchToMongoUpdate(patch);

if (!(field in mongoPatch)) {
return false;
}

return resolveKeyPath(doc, field) !== mongoPatch[field];
};

/**
* Retrieves the property stored at keyPath in the gibven object.
*
* @param obj an object.
* @param keyPath a property path, e.g. `prop.subProp`.
*
* @return the value of the property stored at keyPath within object.
*/
const resolveKeyPath = (obj, keyPath) => {
const properties = keyPath.split('.');
let resolved = { ...obj };
for(const key of properties) {
resolved = resolved[key];
}

return resolved;
};

/**
* Translates the given patch object into a mongdb update.
*
* E.g. replaces subdocuments with `.` key path syntax:
*
* {
* foo: {
* bar: 'baz'
* }
* }
*
* becomes
*
* {
* 'foo.bar': 'baz
* }
*
* prefix param is for recurrsion.
*/
const translatePatchToMongoUpdate = (patch, prefix = '') =>
Object.keys(patch)
.reduce((translatedPatch, key) => {
if (typeof patch[key] === 'object' && !Array.isArray(patch[key]) && !(patch[key] instanceof Date)) {
return {
...translatedPatch,
...translatePatchToMongoUpdate(patch[key], `${prefix}${key}.`)
};
} else {
translatedPatch[`${prefix}${key}`] = patch[key];
return translatedPatch;
}
}, {});

module.exports = ({ mongoUri } = {}) => ({
changes,
cleanup,
client: client.bind(null, mongoUri),
errors,
isError
isError,
patchChangesField,
resolveKeyPath,
translatePatchToMongoUpdate
});

85 changes: 85 additions & 0 deletions mongo.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,90 @@ describe('mongo service', () => {
.toEqual(false);
});
});

describe('resolveKeyPath()', () => {
describe('if the key path does not contain sub keys', () => {
it('returns the property value', () => {
const service = mongo();
const mock = {
foo: {
bar: 'baz'
}
};

expect(service.resolveKeyPath(mock, 'foo.bar'))
.toEqual('baz');
});
});
});

describe('changes()', () => {
describe('when the patch changes a direct descendant field', () => {
it('properly reports the change', () => {
const service = mongo();
const mock = {
foo: {
bar: 'baz'
},
alpha: 3
};

const changes = service.changes(mock, {
alpha: 4
});

expect(changes)
.toContainEqual({
field: 'alpha',
from: 3,
to: 4
});
});
});

describe('when the patch does not change anything', () => {
it('reports no changes', () => {
const service = mongo();
const mock = {
foo: {
bar: 'baz'
},
alpha: 3
};

const changes = service.changes(mock, {
alpha: 3
});

expect(changes.length)
.toEqual(0);
});
});

describe('when the patch changes a subdocument', () => {
it('reports with key paths', () => {
const service = mongo();
const mock = {
foo: {
bar: 'baz'
},
alpha: 3
};

const changes = service.changes(mock, {
foo: {
bar: 'blub'
}
});

expect(changes)
.toContainEqual({
field: 'foo.bar',
from: 'baz',
to: 'blub'
});
});
});
});
});

Loading

0 comments on commit 74a686a

Please sign in to comment.