Skip to content

Commit

Permalink
Added #29 Saving multiple Entities
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga committed Dec 8, 2016
1 parent 2eb7d03 commit b7cdb7c
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 57 deletions.
140 changes: 86 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ This library is in active development, please report any issue you might find.
- [excludeFromIndexes](#excludefromindexes)
- [read](#read)
- [write](#write)
- [required](#required)
- [Schema options](#schema-options)
- [validateBeforeSave (default true)](#validatebeforesave-default-true)
- [explicitOnly (default true)](#explicitonly-default-true)
- [queries](#queries)
- [queries config](#queries-config)
- [Schema methods](#schema-methods)
- [path()](#path)
- [virtual()](#virtual)
- [Custom Methods](#custom-methods)
- [Model](#model)
- [Creation](#creation-1)
- [Methods](#methods)
Expand Down Expand Up @@ -79,7 +81,6 @@ This library is in active development, please report any issue you might find.
- [Pre hooks](#pre-hooks)
- [Post hooks](#post-hooks)
- [Transactions and Hooks](#transactions-and-hooks)
- [Custom Methods](#custom-methods)
- [Credits](#credits)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -375,6 +376,71 @@ user.save(function() {...});

```
### Custom Methods
Custom methods can be attached to entities instances through their Schemas.
`schema.methods.methodName = function(){}`
```js
var blogPostSchema = new Schema({title:{}});

// Custom method to retrieve all children Text entities
blogPostSchema.methods.texts = function(cb) {
var query = this.model('Text')
.query()
.hasAncestor(this.entityKey);

query.run(function(err, result){
if (err) {
return cb(err);
}
cb(null, result.entities);
});
};

...

// You can then call it on an entity instance of BlogPost
BlogPost.get(123).then((data) => {
const blogEntity = data[0];
blogEntity.texts(function(err, texts) {
console.log(texts); // texts entities;
});
});
```
Note how entities instances can access other models through `entity.model('OtherModel')`. *Denormalization* can then easily be done with a custom method:
```js
// Add custom "getImage()" method on the User Schema
userSchema.methods.getImage = function(cb) {
// Any type of query can be done here
// note this.get('imageIdx') could also be accessed by virtual property: this.imageIdx
return this.model('Image').get(this.get('imageIdx'), cb);
};
...
// In your controller
var user = new User({name:'John', imageIdx:1234});

// Call custom Method 'getImage'
user.getImage(function(err, imageEntity) {
user.profilePict = imageEntity.get('url');
user.save().then(() { ... });
});

// Or with Promises
userSchema.methods.getImage = function() {
return this.model('Image').get(this.imageIdx);
};
...
var user = new User({name:'John', imageIdx:1234});
user.getImage().then((data) => {
const imageEntity = data[0];
...
});
```
## Model
### Creation
Expand Down Expand Up @@ -1494,66 +1560,32 @@ transaction.run().then(() => {

```
## Custom Methods
Custom methods can be attached to entities instances.
`schema.methods.methodName = function(){}`
----------
```js
var blogPostSchema = new Schema({title:{}});
## Global Methods
### save()
// Custom method to retrieve all children Text entities
blogPostSchema.methods.texts = function(cb) {
var query = this.model('Text')
.query()
.hasAncestor(this.entityKey);
gstore has a global method "save" that is an alias of the original Datastore save() method, with the exception that you can pass it an Entity **instance** or an **\<Array\>** of entities instances and it will first convert them to the correct Datastore format before saving.
query.run(function(err, result){
if (err) {
return cb(err);
}
cb(null, result.entities);
});
};
**Note**: The entities can be of **any Kind**. You can concat several arrays of queries from different Models and then save them all at once with this method.
...
```js
const query = BlogModel.query().limit(20);
query.run({ format: gstore.Queries.formats.ENTITY })
.then((result) => {
const entities = result[0].entities;

// entities are gstore instances, you can manipulate them
// and then save them by calling:

gstore.save(entities).then(() => {
...
});
})

// You can then call it on an entity instance of BlogPost
BlogPost.get(123).then((data) => {
const blogEntity = data[0];
blogEntity.texts(function(err, texts) {
console.log(texts); // texts entities;
});
});
```
Note how entities instances can access other models through `entity.model('OtherModel')`. *Denormalization* can then easily be done with a custom method:
```js
// Add custom "getImage()" method on the User Schema
userSchema.methods.getImage = function(cb) {
// Any type of query can be done here
// note this.get('imageIdx') could also be accessed by virtual property: this.imageIdx
return this.model('Image').get(this.get('imageIdx'), cb);
};
...
// In your controller
var user = new User({name:'John', imageIdx:1234});
user.getImage(function(err, imageEntity) {
user.set('profilePict', imageEntity.get('url'));
user.save(function(err){...});
});

// Or with Promises
userSchema.methods.getImage = function() {
return this.model('Image').get(this.imageIdx);
};
...
var user = new User({name:'John', imageIdx:1234});
user.getImage().then((data) => {
const imageEntity = data[0];
...
});
```
## Credits
I have been heavily inspired by [Mongoose](https://github.com/Automattic/mongoose) to write gstore. Credits to them for the Schema, Model and Entity
Expand Down
11 changes: 11 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Schema = require('./schema');
const Model = require('./model');
const Queries = require('./queries');
const defaultValues = require('./helpers/defaultValues');
const datastoreSerializer = require('./serializer').Datastore;

const pkg = require('../package.json');

Expand Down Expand Up @@ -96,6 +97,16 @@ class Gstore {
return names;
}

save() {
const args = Array.prototype.slice.apply(arguments);

if (args.length > 0 && !is.fn(args[0])) {
// convert entity instance to Datastore format
args[0] = datastoreSerializer.entitiesToDatastore(args[0]);
}
return this._ds.save.apply(this._ds, args);
}

/**
* Expose the defaultValues constants
*/
Expand Down
26 changes: 26 additions & 0 deletions lib/serializers/datastore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict';

const is = require('is');
const arrify = require('arrify');

function toDatastore(obj, nonIndexed) {
nonIndexed = nonIndexed || [];
const results = [];
Expand Down Expand Up @@ -61,8 +64,31 @@ function fromDatastore(entity, options) {
}
}

/**
* Convert one or several entities instance (gstore) to Datastore format
*
* @param {any} entities Entity(ies) to format
* @returns {array} the formated entity(ies)
*/
function entitiesToDatastore(entities) {
const multiple = is.array(entities);
entities = arrify(entities);

if (entities[0].className !== 'Entity') {
// Not an entity instance, nothing to do here...
return entities;
}

const result = entities.map(entity => ({
key: entity.entityKey,
data: toDatastore(entity.entityData, entity.excludeFromIndexes),
}));

return multiple ? result : result[0];
}

module.exports = {
toDatastore,
fromDatastore,
entitiesToDatastore,
};
69 changes: 67 additions & 2 deletions test/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

const chai = require('chai');
const sinon = require('sinon');
const gstore = require('../lib');
const pkg = require('../package.json');

const expect = chai.expect;
const assert = chai.assert;
Expand All @@ -14,8 +12,24 @@ const ds = require('@google-cloud/datastore')({
apiEndpoint: 'http://localhost:8080',
});

const gstore = require('../lib');
const Schema = require('../lib').Schema;
const pkg = require('../package.json');

describe('gstore-node', () => {
let schema;
let ModelInstance;

beforeEach(() => {
gstore.models = {};
gstore.modelSchemas = {};

schema = new Schema({
name: { type: 'string' },
email: { type: 'string', read: false },
});
ModelInstance = gstore.model('Blog', schema, {});
});

it('should initialized its properties', () => {
assert.isDefined(gstore.models);
Expand Down Expand Up @@ -143,4 +157,55 @@ describe('gstore-node', () => {
expect(ds.transaction.called).equal(true);
expect(transaction.constructor.name).equal('Transaction');
});

describe('save() alias', () => {
beforeEach(() => {
sinon.stub(ds, 'save').resolves();
gstore.connect(ds);
});

afterEach(() => {
ds.save.restore();
});

it('should call datastore save passing the arguments', () => (
gstore.save([1, 2, 3]).then(() => {
expect(ds.save.called).equal(true);
expect(ds.save.getCall(0).args).deep.equal([[1, 2, 3]]);
})
));

it('should convert entity instances to datastore Format', () => {
const model1 = new ModelInstance({ name: 'John' });
const model2 = new ModelInstance({ name: 'Mick' });

return gstore.save([model1, model2]).then(() => {
const args = ds.save.getCall(0).args;
const firstEntity = args[0][0];
assert.isUndefined(firstEntity.className);
expect(Object.keys(firstEntity)).deep.equal(['key', 'data']);
});
});

it('should also work with a callback', () => {
ds.save.restore();

sinon.stub(ds, 'save', (entity, cb) => cb());

const model = new ModelInstance({ name: 'John' });

return gstore.save(model, () => {
const args = ds.save.getCall(0).args;
const firstEntity = args[0];
assert.isUndefined(firstEntity.className);
expect(Object.keys(firstEntity)).deep.equal(['key', 'data']);
});
});

it('should forward if no arguments', () => (
gstore.save().then(() => {
expect(ds.save.getCall(0).args.length).equal(0);
})
));
});
});
1 change: 0 additions & 1 deletion test/serializers/datastore-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ describe('Datastore serializer', () => {
.call(ModelInstance, datastoreMock, { format: gstore.Queries.formats.ENTITY });

expect(serialized.className).equal('Entity');
expect(serialized.constructor.name).equal('ModelInstance');
});
});

Expand Down

0 comments on commit b7cdb7c

Please sign in to comment.