@@ -5,10 +5,12 @@ const is = require('is');
55const hooks = require ( 'promised-hooks' ) ;
66const arrify = require ( 'arrify' ) ;
77
8- const utils = require ( './utils' ) ;
98const datastoreSerializer = require ( './serializer' ) . Datastore ;
109const defaultValues = require ( './helpers/defaultValues' ) ;
1110const { errorCodes } = require ( './errors' ) ;
11+ const { validation, populateHelpers } = require ( './helpers' ) ;
12+
13+ const { populateFactory } = populateHelpers ;
1214
1315class 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-
318491module . exports = Entity ;
0 commit comments