Skip to content

Commit adb9165

Browse files
committed
Support for firestore
1 parent 2afe227 commit adb9165

File tree

6 files changed

+350
-9
lines changed

6 files changed

+350
-9
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ before_install:
1313
- tar -xzf /tmp/dynamodb_local_latest.tar.gz -C /tmp
1414
- java -Djava.library.path=/tmp/DynamoDBLocal_lib -jar /tmp/DynamoDBLocal.jar -inMemory &
1515
- sleep 2
16+
- echo $BASE64_GOOGLE_KEY | base64 --decode > /tmp/firestore-node-viewmodel-key.json
1617

1718
services:
1819
- mongodb
@@ -40,3 +41,4 @@ env:
4041
- AWS_SECRET_ACCESS_KEY=SECRET
4142
- AWS_REGION=us-east-1
4243
- AWS_DYNAMODB_ENDPOINT=http://localhost:8000
44+
- GOOGLE_APPLICATION_CREDENTIALS=/tmp/firestore-node-viewmodel-key.json

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,57 @@ Additionaly for elasticsearch6 the number of shards, number of replicas, the ref
294294
});
295295
```
296296
297+
## Firestore
298+
299+
### Setup
300+
301+
#### Library installation
302+
303+
`yarn add @google-cloud/firestore` or `npm install --save @google-cloud/firestore`
304+
305+
#### Options
306+
307+
Use the `firestore` type to support Google's Firestore database, and store your KeyFile in a location accessible by your application.
308+
309+
```javascript
310+
const options = {
311+
repository: {
312+
type: 'firestore',
313+
projectId: 'YOUR_PROJECT_ID',
314+
keyFilename: '/path/to/keyfile.json'
315+
},
316+
};
317+
```
318+
319+
### Find Queries
320+
321+
Simple (equality comparison) find queries are supported by passing javascript objects as the query parameter, or more complex queries can be executed via nested arrays. In the case of multiple key/value pairs or nested arrays, the composite predicates form logical ANDs.
322+
323+
``` javascript
324+
// Simple Object format
325+
aRepo.find({'aProp': 'aValue', 'secondProp': 'secondValue'}, function (err, vm) {
326+
if (err) {
327+
console.log('Repo find error', err);
328+
return;
329+
}
330+
console.log('Found', vm);
331+
});
332+
333+
// Nested array syntax, allows for more complex predicates
334+
aRepo.find([['aProp', '==', 'aValue'], ['secondProp', '<', 10000]], function (err, vm) {
335+
if (err) {
336+
console.log('Repo find error', err);
337+
return;
338+
}
339+
console.log('Found', vm);
340+
});
341+
```
342+
343+
The queryOptions parameter supports limit, skip, and sort, in a mongoDb-like syntax.
344+
345+
### Testing Setup
346+
347+
To provide the authentication file to tests, the `GOOGLE_APPLICATION_CREDENTIALS` environment setting should point to the file so it can be loaded by firestore. To inject the file in TravisCI, create a new environment variable called `BASE64_GOOGLE_KEY` in the Travis GUI, and set the value of this to be the Base64 encoded content of the file. The .travis.yml file contains configuration to decode this setting and write it out to a known location for the CI settings to pickup.
297348
298349
# [Release notes](https://github.com/adrai/node-viewmodel/blob/master/releasenotes.md)
299350
@@ -310,6 +361,7 @@ Currently these databases are supported:
310361
8. elasticsearch ([elasticsearch] (https://github.com/elastic/elasticsearch-js))
311362
9. elasticsearch6 ([elasticsearch] (https://github.com/elastic/elasticsearch-js)) - for Elasticsearch 5.x and 6.x
312363
10. dynamodb ([aws-sdk] (https://github.com/aws/aws-sdk-js))
364+
11. firestore ([@google-cloud/firestore] (https://github.com/googleapis/nodejs-firestore))
313365
314366
## own db implementation
315367
You can use your own db implementation by extending this...

lib/databases/firestore.js

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
var util = require('util'),
2+
_ = require('lodash'),
3+
async = require('async'),
4+
ConcurrencyError = require('../concurrencyError'),
5+
gcFirestore = require('@google-cloud/firestore'),
6+
Repository = require('../base'),
7+
uuid = require('uuid').v4,
8+
ViewModel = Repository.ViewModel;
9+
10+
var collections = [];
11+
12+
function Firestore(options) {
13+
Repository.call(this);
14+
this.options = _.merge({ timestampsInSnapshots: true }, options);
15+
}
16+
17+
util.inherits(Firestore, Repository);
18+
19+
function implementError (callback) {
20+
var err = new Error('Storage method add is not implemented');
21+
if (callback) callback(err);
22+
throw err;
23+
}
24+
25+
function parseFirestoreQuery(query) {
26+
if (_.isArray(query)) {
27+
return query;
28+
} else if (_.isPlainObject(query)) {
29+
return _.map(query, function(value, key) {
30+
return [key, '==', value];
31+
});
32+
}
33+
throw new Error('Unknown query type');
34+
};
35+
36+
function firestoreQueryParser(collectionRef, queryParams) {
37+
var params = parseFirestoreQuery(queryParams);
38+
return _.reduce(params, function(acc, q) {
39+
return acc.where.apply(acc, q);
40+
}, collectionRef);
41+
};
42+
43+
function emptyCollection(db, collection, callback) {
44+
var collectionRef = db.collection(collection);
45+
var query = collectionRef.get().then(function (querySnapshot) {
46+
var writeBatch = db.batch();
47+
querySnapshot.forEach(function (documentSnapshot) {
48+
var documentPath = collection + '/' + documentSnapshot.id;
49+
var documentRef = db.doc(documentPath);
50+
writeBatch.delete(documentRef);
51+
});
52+
writeBatch.commit().then(function () {
53+
if (callback) callback(null);
54+
});
55+
});
56+
};
57+
58+
function getPrecondition(vm) {
59+
var precondition = {};
60+
if (!_.isUndefined(vm.get('_updateTime'))) {
61+
const time = vm.get('_updateTime');
62+
if (_.isDate(time)) {
63+
precondition['lastUpdateTime'] = time.toISOString();
64+
} else if (_.isString(time)) {
65+
precondition['lastUpdateTime'] = time;
66+
}
67+
}
68+
return precondition;
69+
}
70+
71+
function enrichVMWithTimestamps(vm, documentSnapshot) {
72+
_.isUndefined(documentSnapshot.readTime) ? false : vm.set('_readTime', documentSnapshot.readTime);
73+
_.isUndefined(documentSnapshot.createTime) ? false : vm.set('_createTime', documentSnapshot.createTime);
74+
_.isUndefined(documentSnapshot.updateTime) ? false : vm.set('_updateTime', documentSnapshot.updateTime);
75+
return vm;
76+
};
77+
78+
function applyQueryOptions(query, options) {
79+
if (!_.isUndefined(options)) {
80+
// Apply supported queryOptions
81+
if (_.has(options, 'limit')) {
82+
query = query.limit(options.limit);
83+
}
84+
if (_.has(options, 'skip')) {
85+
query = query.offset(options.skip);
86+
}
87+
if (_.has(options, 'sort')) {
88+
var sortKey = options.sort.keys[0];
89+
var direction = options.sort.keys[sortKey] == 1 ? 'asc' : 'desc';
90+
query = query.orderBy(sortKey, direction);
91+
}
92+
}
93+
return query;
94+
}
95+
96+
_.extend(Firestore.prototype, {
97+
98+
connect: function (callback) {
99+
var self = this;
100+
var options = this.options;
101+
self.db = new gcFirestore(options);
102+
self.emit('connect');
103+
if (callback) callback(null, self);
104+
},
105+
106+
disconnect: function (callback) {
107+
var self = this;
108+
delete self.db;
109+
self.emit('disconnect');
110+
if (callback) callback(null, self);
111+
},
112+
113+
getNewId: function (callback) {
114+
this.checkConnection();
115+
116+
var id = uuid().toString();
117+
if (callback) callback(null, id);
118+
},
119+
120+
get: function (id, callback) {
121+
this.checkConnection();
122+
123+
if(_.isFunction(id)) {
124+
callback = id;
125+
id = null;
126+
}
127+
128+
if (!id) {
129+
id = uuid().toString();
130+
}
131+
132+
var self = this;
133+
134+
var documentPath = this.collection + '/' + id;
135+
var documentRef = this.db.doc(documentPath);
136+
137+
documentRef.get().then(function (documentSnapshot) {
138+
var vm = new ViewModel(documentSnapshot.data() || { id }, self);
139+
vm = enrichVMWithTimestamps(vm, documentSnapshot);
140+
if (documentSnapshot.exists) {
141+
vm.actionOnCommit = 'update';
142+
} else {
143+
vm.actionOnCommit = 'create';
144+
}
145+
callback(null, vm);
146+
});
147+
},
148+
149+
find: function (queryParams, queryOptions, callback) {
150+
this.checkConnection();
151+
152+
var self = this;
153+
var collectionRef = this.db.collection(this.collection);
154+
155+
var query = firestoreQueryParser(collectionRef, queryParams);
156+
query = applyQueryOptions(query, queryOptions);
157+
158+
query.get().then(function (querySnapshot) {
159+
var vms = _.map(querySnapshot.docs, function(documentSnapshot) {
160+
var vm = new ViewModel(documentSnapshot.data(), self);
161+
vm = enrichVMWithTimestamps(vm, documentSnapshot);
162+
vm.actionOnCommit = 'update';
163+
return vm;
164+
});
165+
callback(null, vms);
166+
});
167+
},
168+
169+
findOne: function (queryParams, queryOptions, callback) {
170+
// NOTE: queryOptions is ignored
171+
this.checkConnection();
172+
173+
var self = this;
174+
var collectionRef = this.db.collection(this.collection);
175+
176+
var query = firestoreQueryParser(collectionRef, queryParams);
177+
_.unset(queryOptions, 'limit');
178+
query = applyQueryOptions(query, queryOptions);
179+
query.limit(1).get().then(function (querySnapshot) {
180+
if (querySnapshot.size == 0) {
181+
callback(null, null);
182+
}
183+
querySnapshot.forEach(function (documentSnapshot) {
184+
var vm = new ViewModel(documentSnapshot.data(), self);
185+
vm = enrichVMWithTimestamps(vm, documentSnapshot);
186+
vm.actionOnCommit = 'update';
187+
callback(null, vm);
188+
});
189+
});
190+
},
191+
192+
commit: function (vm, callback) {
193+
this.checkConnection();
194+
195+
if (!vm.actionOnCommit) return callback(new Error('actionOnCommit is not defined!'));
196+
197+
var self = this;
198+
199+
switch(vm.actionOnCommit) {
200+
case 'delete':
201+
var documentPath = this.collection + '/' + vm.id;
202+
var documentRef = this.db.doc(documentPath);
203+
var precondition = getPrecondition(vm);
204+
documentRef.delete(precondition).then(function () {
205+
callback(null);
206+
}).catch(function (err) {
207+
return callback(new ConcurrencyError());
208+
});
209+
break;
210+
case 'create':
211+
var documentPath = this.collection + '/' + vm.id;
212+
var documentRef = this.db.doc(documentPath);
213+
documentRef.get().then(function (documentSnapshot) {
214+
if (documentSnapshot.exists) {
215+
return callback(new ConcurrencyError());
216+
}
217+
documentRef.set(vm.attributes).then(function () {
218+
vm.actionOnCommit = 'update';
219+
callback(null, vm);
220+
});
221+
});
222+
break;
223+
case 'update':
224+
var documentPath = this.collection + '/' + vm.id;
225+
var documentRef = this.db.doc(documentPath);
226+
documentRef.get().then(function (documentSnapshot) {
227+
if (!documentSnapshot.exists) {
228+
documentRef.set(vm.attributes).then(function () {
229+
vm.actionOnCommit = 'update';
230+
callback(null, vm);
231+
});
232+
} else {
233+
if (!_.isUndefined(documentSnapshot.updateTime) &&
234+
_.isUndefined(vm.get('_updateTime'))) {
235+
return callback(new ConcurrencyError());
236+
}
237+
238+
var precondition = getPrecondition(vm);
239+
documentRef.update(vm.attributes, precondition).then(function () {
240+
self.get(vm.id, callback);
241+
}, function (err) {
242+
return callback(new ConcurrencyError());
243+
});
244+
}
245+
});
246+
break;
247+
default:
248+
return callback(new Error('Unknown actionOnCommit: ' + vm.actionOnCommit));
249+
};
250+
},
251+
252+
checkConnection: function (callback) {
253+
if (this.collection) {
254+
return;
255+
}
256+
257+
if (collections.indexOf(this.collectionName) < 0) {
258+
collections.push(this.collectionName)
259+
}
260+
261+
this.collection = this.collectionName;
262+
if (callback) callback(null);
263+
},
264+
265+
clear: function (callback) {
266+
this.checkConnection();
267+
268+
var self = this;
269+
if (!this.collection) {
270+
if (callback) callback(null);
271+
return;
272+
}
273+
274+
emptyCollection(this.db, this.collection, callback);
275+
},
276+
277+
clearAll: function (callback) {
278+
var self = this;
279+
async.each(collections, function (col, callback) {
280+
emptyCollection(self.db, col, callback);
281+
}, callback);
282+
},
283+
284+
});
285+
286+
module.exports = Firestore;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"uuid": "3.1.0"
2222
},
2323
"devDependencies": {
24+
"@google-cloud/firestore": "^0.16.1",
2425
"aws-sdk": ">=2.123.0",
2526
"azure-storage": ">=0.3.0",
2627
"cradle": ">=0.6.7",

0 commit comments

Comments
 (0)