Skip to content

Commit 72fff67

Browse files
authored
feat(Populate): Fetch entities references in Model.get() and queries
If an entity property value is a reference Key to another entity, we can now populate that other entity by chaining .populate(<refs>, <properties>) calls to Model.get(), queries and on entity instances. BREAKING CHANGE: Callback (hell) are not supported anymore as the last argument of gstore methods. Only Promises are returned. BREAKING CHANGE: Node runtime must be version 8 or superior BREAKING CHANGE: The old Schema property types "datetime" and "int" have been removed. Date and Number types should be used instead.
1 parent 850019e commit 72fff67

30 files changed

+1922
-1191
lines changed

.eslintrc.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"parserOptions": {
3-
"ecmaVersion": 6,
3+
"ecmaVersion": 2018,
44
"sourceType": "script"
55
},
66
"root": true,
@@ -43,7 +43,7 @@
4343
"error",
4444
120
4545
],
46-
"mocha/no-exclusive-tests": "off",
46+
"mocha/no-exclusive-tests": "error",
4747
"comma-dangle": [
4848
"error",
4949
{

.travis.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
language: node_js
22
node_js:
3-
- "10"
3+
- "lts/*"
44
- "8"
5-
- "6"
65

76
branches:
87
only:
98
- master
109

11-
install: yarn install --ignore-engines
10+
install: yarn install
1211

1312
script:
1413
- npm test
1514

1615
after_success:
1716
- npm run coveralls
1817

19-
sudo: false
18+
sudo: required
2019
before_install:
21-
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.12.3
20+
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.14.0
2221
- export PATH=$HOME/.yarn/bin:$PATH
22+
- yarn global add node-gyp node-pre-gyp
23+
24+
env:
25+
- CXX=g++-5
26+
27+
# https://stackoverflow.com/a/40802733/5642633
28+
addons:
29+
apt:
30+
sources:
31+
- ubuntu-toolchain-r-test
32+
packages:
33+
- g++-5
2334
cache:
2435
yarn: true
36+

lib/dataloader.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
const optional = require('optional');
66
const dsAdapter = require('nsql-cache-datastore')();
7+
const arrify = require('arrify');
78

89
const DataLoader = optional('dataloader');
910
const { keyToString } = dsAdapter;
@@ -20,8 +21,12 @@ function createDataLoader(ds) {
2021
}
2122

2223
return new DataLoader(keys => (
23-
ds.get(keys).then((res) => {
24-
const entities = res[0];
24+
ds.get(keys).then(([res]) => {
25+
// When providing an Array with 1 Key item, google-datastore
26+
// returns a single item.
27+
// For predictable results in gstore, all responses from Datastore.get()
28+
// calls return an Array
29+
const entities = arrify(res);
2530
const entitiesByKey = {};
2631
entities.forEach((entity) => {
2732
entitiesByKey[keyToString(entity[ds.KEY])] = entity;

lib/entity.js

Lines changed: 191 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ const is = require('is');
55
const hooks = require('promised-hooks');
66
const arrify = require('arrify');
77

8-
const utils = require('./utils');
98
const datastoreSerializer = require('./serializer').Datastore;
109
const defaultValues = require('./helpers/defaultValues');
1110
const { errorCodes } = require('./errors');
11+
const { validation, populateHelpers } = require('./helpers');
12+
13+
const { populateFactory } = populateHelpers;
1214

1315
class Entity {
1416
constructor(data, id, ancestors, namespace, key) {
@@ -43,6 +45,176 @@ class Entity {
4345
registerHooksFromSchema(this);
4446
}
4547

48+
save(transaction, opts = {}) {
49+
this.__hooksEnabled = true;
50+
const _this = this;
51+
const options = {
52+
method: 'upsert',
53+
...opts,
54+
};
55+
56+
let { error } = validateEntityData();
57+
58+
if (!error) {
59+
({ error } = validateMethod(options.method));
60+
}
61+
62+
if (error) {
63+
return Promise.reject(error);
64+
}
65+
66+
this.entityData = prepareData.call(this);
67+
68+
const entity = datastoreSerializer.toDatastore(this);
69+
entity.method = options.method;
70+
71+
if (!transaction) {
72+
return this.gstore.ds.save(entity).then(onSuccess);
73+
}
74+
75+
if (transaction.constructor.name !== 'Transaction') {
76+
return Promise.reject(new Error('Transaction needs to be a gcloud Transaction'));
77+
}
78+
79+
addPostHooksTransaction.call(this);
80+
transaction.save(entity);
81+
82+
return Promise.resolve(this);
83+
84+
// --------------------------
85+
86+
function onSuccess() {
87+
/**
88+
* Make sure to clear the cache for this Entity Kind
89+
*/
90+
if (_this.constructor.__hasCache(options)) {
91+
return _this.constructor.clearCache()
92+
.then(() => _this)
93+
.catch((err) => {
94+
let msg = 'Error while clearing the cache after saving the entity.';
95+
msg += 'The entity has been saved successfully though. ';
96+
msg += 'Both the cache error and the entity saved have been attached.';
97+
const cacheError = new Error(msg);
98+
cacheError.__entity = _this;
99+
cacheError.__cacheError = err;
100+
throw cacheError;
101+
});
102+
}
103+
104+
return _this;
105+
}
106+
107+
function validateEntityData() {
108+
if (_this.schema.options.validateBeforeSave) {
109+
return _this.validate();
110+
}
111+
112+
return {};
113+
}
114+
115+
function validateMethod(method) {
116+
const allowed = {
117+
update: true,
118+
insert: true,
119+
upsert: true,
120+
};
121+
122+
return !allowed[method]
123+
? { error: new Error('Method must be either "update", "insert" or "upsert"') }
124+
: { error: null };
125+
}
126+
127+
/**
128+
* Process some basic formatting to the entity data before save
129+
* - automatically set the modifiedOn property to current date (if exists on schema)
130+
* - convert object with latitude/longitude to Datastore GeoPoint
131+
*/
132+
function prepareData() {
133+
updateModifiedOn.call(this);
134+
convertGeoPoints.call(this);
135+
136+
return this.entityData;
137+
138+
//--------------------------
139+
140+
/**
141+
* If the schema has a "modifiedOn" property we automatically
142+
* update its value to the current dateTime
143+
*/
144+
function updateModifiedOn() {
145+
if ({}.hasOwnProperty.call(this.schema.paths, 'modifiedOn')) {
146+
this.entityData.modifiedOn = new Date();
147+
}
148+
}
149+
150+
/**
151+
* If the entityData has some property of type 'geoPoint'
152+
* and its value is an js object with "latitude" and "longitude"
153+
* we convert it to a datastore GeoPoint.
154+
*/
155+
function convertGeoPoints() {
156+
if (!{}.hasOwnProperty.call(this.schema.__meta, 'geoPointsProps')) {
157+
return;
158+
}
159+
160+
this.schema.__meta.geoPointsProps.forEach((property) => {
161+
if ({}.hasOwnProperty.call(_this.entityData, property)
162+
&& _this.entityData[property] !== null
163+
&& _this.entityData[property].constructor.name !== 'GeoPoint') {
164+
_this.entityData[property] = _this.gstore.ds.geoPoint(_this.entityData[property]);
165+
}
166+
});
167+
}
168+
}
169+
170+
/**
171+
* If it is a transaction, we create a hooks.post array that will be executed
172+
* when transaction succeeds by calling transaction.execPostHooks() (returns a Promises)
173+
*/
174+
function addPostHooksTransaction() {
175+
// disable (post) hooks, we will only trigger them on transaction succceed
176+
this.__hooksEnabled = false;
177+
this.constructor.hooksTransaction.call(
178+
_this,
179+
transaction,
180+
this.__posts
181+
? this.__posts.save
182+
: undefined
183+
);
184+
}
185+
}
186+
187+
validate() {
188+
const { schema, entityKind } = this;
189+
const { entityData } = this;
190+
const key = this.entityData[this.gstore.ds.KEY]; // save the Key
191+
192+
/**
193+
* If not a Joi schema, we sanitize the entityData.
194+
* If it's a Joi, it will be done automatically when validating.
195+
*/
196+
if (is.undef(schema._joi)) {
197+
this.entityData = this.constructor.sanitize(entityData, { disabled: ['write'] });
198+
}
199+
200+
const validationResult = validation.validate(
201+
this.entityData,
202+
schema,
203+
entityKind,
204+
this.gstore.ds
205+
);
206+
207+
/**
208+
* If it's a Joi schema, make sure to update the entityData object
209+
*/
210+
if (is.defined(schema._joi)) {
211+
this.entityData = validationResult.value;
212+
}
213+
214+
this.entityData[this.gstore.ds.KEY] = key; // put the Key back
215+
return validationResult;
216+
}
217+
46218
plain(options) {
47219
options = typeof options === 'undefined' ? {} : options;
48220

@@ -91,17 +263,15 @@ class Entity {
91263
*
92264
* @param {Function} cb Callback
93265
*/
94-
datastoreEntity(...args) {
266+
datastoreEntity(options = {}) {
95267
const _this = this;
96-
const cb = args.pop();
97-
const options = args[0] || {};
98268

99269
if (this.constructor.__hasCache(options)) {
100270
return this.gstore.cache.keys
101271
.read(this.entityKey, options)
102-
.then(onSuccess, onError);
272+
.then(onSuccess);
103273
}
104-
return this.gstore.ds.get(this.entityKey).then(onSuccess, onError);
274+
return this.gstore.ds.get(this.entityKey).then(onSuccess);
105275

106276
// ------------------------
107277

@@ -110,22 +280,28 @@ class Entity {
110280

111281
if (!datastoreEntity) {
112282
if (_this.gstore.config.errorOnEntityNotFound) {
113-
return cb({
114-
code: errorCodes.ERR_ENTITY_NOT_FOUND,
115-
message: 'Entity not found',
116-
});
283+
const error = new Error('Entity not found');
284+
error.code = errorCodes.ERR_ENTITY_NOT_FOUND;
285+
throw error;
117286
}
118287

119-
return cb(null, null);
288+
return null;
120289
}
121290

122291
_this.entityData = datastoreEntity;
123-
return cb(null, _this);
292+
return _this;
124293
}
294+
}
125295

126-
function onError(err) {
127-
return cb(err);
128-
}
296+
populate(path, propsToSelect) {
297+
const refsToPopulate = [];
298+
299+
const promise = Promise.resolve(this)
300+
.then(this.constructor.populate(refsToPopulate));
301+
302+
promise.populate = populateFactory(refsToPopulate, promise, this.constructor);
303+
promise.populate(path, propsToSelect);
304+
return promise;
129305
}
130306

131307
getEntityDataWithVirtuals() {
@@ -312,7 +488,4 @@ function registerHooksFromSchema(self) {
312488
return self;
313489
}
314490

315-
// Promisify Entity methods
316-
Entity.prototype.datastoreEntity = utils.promisify(Entity.prototype.datastoreEntity);
317-
318491
module.exports = Entity;

lib/helpers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
const queryHelpers = require('./queryhelpers');
55
const validation = require('./validation');
6+
const populateHelpers = require('./populateHelpers');
67
// const googleCloud = require('./google-cloud');
78

89
module.exports = {
910
queryHelpers,
1011
validation,
12+
populateHelpers,
1113
// googleCloud,
1214
};

0 commit comments

Comments
 (0)