Skip to content

Commit cd2c12f

Browse files
authored
Transform datastore event data format to be more user friendly. (#148)
1 parent ae884e9 commit cd2c12f

File tree

3 files changed

+238
-4
lines changed

3 files changed

+238
-4
lines changed

spec/providers/datastore.spec.ts

+162-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('Datastore Functions', () => {
3333
delete process.env.GCLOUD_PROJECT;
3434
});
3535

36-
describe('DataConstructors', () => {
36+
describe('document builders', () => {
3737
function expectedTrigger(resource: string) {
3838
return {
3939
eventTrigger: {
@@ -67,4 +67,165 @@ describe('Datastore Functions', () => {
6767
expect(cloudFunction.__trigger).to.deep.equal(expectedTrigger(resource));
6868
});
6969
});
70+
71+
describe('dataConstructor', () => {
72+
let testEvent = {
73+
'data': {
74+
'oldValue': {
75+
'fields': {
76+
'key1': {
77+
'booleanValue': false,
78+
},
79+
'key2': {
80+
'integerValue': '111',
81+
},
82+
},
83+
},
84+
'value': {
85+
'fields': {
86+
'key1': {
87+
'booleanValue': true,
88+
},
89+
'key2': {
90+
'integerValue': '123',
91+
},
92+
},
93+
},
94+
},
95+
};
96+
97+
it('constructs appropriate fields and getters for event.data', () => {
98+
let testFunction = datastore.document('path').onWrite((event) => {
99+
expect(event.data.data()).to.deep.equal({key1: true, key2: 123});
100+
expect(event.data.get('key1')).to.equal(true);
101+
expect(event.data.previous.data()).to.deep.equal({key1: false, key2: 111});
102+
expect(event.data.previous.get('key1')).to.equal(false);
103+
});
104+
return testFunction(testEvent);
105+
});
106+
});
107+
108+
describe('DeltaDocumentSnapshot', () => {
109+
it('should parse int values', () => {
110+
let snapshot = new datastore.DeltaDocumentSnapshot({'key': {'integerValue': '123'}}, {});
111+
expect(snapshot.data()).to.deep.equal({'key': 123});
112+
});
113+
114+
it('should parse double values', () => {
115+
let snapshot = new datastore.DeltaDocumentSnapshot({'key': {'doubleValue': 12.34}}, {});
116+
expect(snapshot.data()).to.deep.equal({'key': 12.34});
117+
});
118+
119+
it('should parse long values', () => {
120+
let snapshot = new datastore.DeltaDocumentSnapshot({'key': {'longValue': 12.34}}, {});
121+
expect(snapshot.data()).to.deep.equal({'key': 12.34});
122+
});
123+
124+
it('should parse null values', () => {
125+
let snapshot = new datastore.DeltaDocumentSnapshot({'key': {'nullValue': null}}, {});
126+
expect(snapshot.data()).to.deep.equal({'key': null});
127+
});
128+
129+
it('should parse boolean values', () => {
130+
let snapshot = new datastore.DeltaDocumentSnapshot({'key': {'booleanValue': true}}, {});
131+
expect(snapshot.data()).to.deep.equal({'key': true});
132+
});
133+
134+
it('should parse string values', () => {
135+
let snapshot = new datastore.DeltaDocumentSnapshot({'key': {'stringValue': 'foo'}}, {});
136+
expect(snapshot.data()).to.deep.equal({'key': 'foo'});
137+
});
138+
139+
it('should parse array values', () => {
140+
let raw = {'key': {
141+
'arrayValue': {
142+
'values': [
143+
{ 'integerValue': '1' },
144+
{ 'integerValue': '2' },
145+
],
146+
},
147+
}};
148+
let snapshot = new datastore.DeltaDocumentSnapshot(raw, {});
149+
expect(snapshot.data()).to.deep.equal({'key': [1, 2]});
150+
});
151+
152+
it('should parse object values', () => {
153+
let raw = {'keyParent': {
154+
'mapValue': {
155+
'fields': {
156+
'key1': {
157+
'stringValue': 'val1',
158+
},
159+
'key2': {
160+
'stringValue': 'val2',
161+
},
162+
},
163+
},
164+
}};
165+
let snapshot = new datastore.DeltaDocumentSnapshot(raw, {});
166+
expect(snapshot.data()).to.deep.equal({'keyParent': {'key1':'val1', 'key2':'val2'}});
167+
});
168+
169+
it('should parse GeoPoint values', () => {
170+
let raw = {
171+
'geoPointValue': {
172+
'mapValue': {
173+
'fields': {
174+
'latitude': {
175+
'doubleValue': 40.73,
176+
},
177+
'longitude': {
178+
'doubleValue': -73.93,
179+
},
180+
},
181+
},
182+
},
183+
};
184+
let snapshot = new datastore.DeltaDocumentSnapshot(raw, {});
185+
expect(snapshot.data()).to.deep.equal({'geoPointValue': {
186+
'latitude': 40.73,
187+
'longitude': -73.93,
188+
}});
189+
});
190+
191+
it('should parse reference values', () => {
192+
let raw = {
193+
'referenceVal': {
194+
'referenceValue': 'projects/proj1/databases/(default)/documents/doc1/id',
195+
},
196+
};
197+
let snapshot = new datastore.DeltaDocumentSnapshot(raw, {});
198+
// TODO: need to actually construct a reference
199+
expect(snapshot.data()).to.deep.equal({
200+
'referenceVal': 'projects/proj1/databases/(default)/documents/doc1/id',
201+
});
202+
});
203+
204+
it('should parse timestamp values', () => {
205+
let raw = {
206+
'timestampVal': {
207+
'timestampValue': '2017-06-13T00:58:40.349Z',
208+
},
209+
};
210+
let snapshot = new datastore.DeltaDocumentSnapshot(raw, {});
211+
expect(snapshot.data()).to.deep.equal({'timestampVal': new Date('2017-06-13T00:58:40.349Z')});
212+
});
213+
214+
it('should parse binary values', () => {
215+
// Format defined in https://developers.google.com/discovery/v1/type-format
216+
let raw = {
217+
'binaryVal': {
218+
'bytesValue': 'Zm9vYmFy',
219+
},
220+
};
221+
let snapshot = new datastore.DeltaDocumentSnapshot(raw, {});
222+
let binaryVal;
223+
try {
224+
binaryVal = Buffer.from('Zm9vYmFy', 'base64');
225+
} catch (e) { // Node version < 6, which is the case for Travis CI
226+
binaryVal = new Buffer('Zm9vYmFy', 'base64');
227+
}
228+
expect(snapshot.data()).to.deep.equal({'binaryVal': binaryVal});
229+
});
230+
});
70231
});

src/providers/README.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,4 @@ Check out [the Cloud Functions for Firebase documentation](https://firebase.goog
55
The Datastore integration is in early preview, and not yet publicly usable.
66
If you'd like to request early access to use this integration, [please fill out this form](https://services.google.com/fb/forms/firebasealphaprogram/).
77

8-
Since this is a preview feature, any new version may introduce breaking changes.
9-
It is recommended that you depend on a specific version of the firebase-functions SDK
10-
in your project's functions/package.json.
8+
There will be a breaking change in the type returned by event.data.data() in the future. Values that represent database references are currently path strings. A future revision update to the SDK will change this to reference objects. To protect against this and other unintended breaking changes, consider depending on an exact version of the firebase-functions SDK in your package.json.

src/providers/datastore.ts

+75
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// SOFTWARE.
2222

23+
import * as _ from 'lodash';
2324
import { makeCloudFunction, CloudFunction, Event } from '../cloud-functions';
2425

2526
/** @internal */
@@ -69,10 +70,84 @@ export class DocumentBuilder {
6970
}
7071

7172
onWrite(handler: (event: Event<any>) => PromiseLike<any> | any): CloudFunction<any> {
73+
const dataConstructor = (raw: Event<any>) => {
74+
if (raw.data instanceof DeltaDocumentSnapshot) {
75+
return raw.data;
76+
}
77+
return new DeltaDocumentSnapshot(
78+
_.get(raw.data, 'value.fields', {}),
79+
_.get(raw.data, 'oldValue.fields', {})
80+
);
81+
};
7282
return makeCloudFunction({
7383
provider, handler,
7484
resource: this.resource,
7585
eventType: 'document.write',
86+
dataConstructor,
7687
});
7788
}
7889
}
90+
91+
export class DeltaDocumentSnapshot {
92+
93+
private _data: object;
94+
private _previous: DeltaDocumentSnapshot;
95+
96+
constructor(private _raw: object, private _old: object) { }
97+
98+
data(): object {
99+
if (!this._data) {
100+
this._data = _.mapValues(this._raw, (field) => {
101+
return this._transformField(field);
102+
});
103+
}
104+
return this._data;
105+
}
106+
107+
get(key: string): any {
108+
return _.get(this.data(), key, null);
109+
}
110+
111+
get previous(): DeltaDocumentSnapshot {
112+
if (_.isEmpty(this._old)) {
113+
return null;
114+
}
115+
this._previous = new DeltaDocumentSnapshot(this._old, null);
116+
return this._previous;
117+
}
118+
119+
private _transformField(field: object): any {
120+
// field is an object with only 1 key-value pair, so this will only loop once
121+
let result;
122+
_.forEach(field, (fieldValue, fieldType) => {
123+
if (fieldType === 'arrayValue') {
124+
result = _.map(_.get(fieldValue, 'values', []), (elem) => {
125+
return this._transformField(elem);
126+
});
127+
} else if (fieldType === 'mapValue') {
128+
result = _.mapValues(_.get(fieldValue, 'fields', {}), (val) => {
129+
return this._transformField(val);
130+
});
131+
} else if (fieldType === 'integerValue'
132+
|| fieldType === 'doubleValue'
133+
|| fieldType === 'longValue') {
134+
result = Number(fieldValue);
135+
} else if (fieldType === 'timestampValue') {
136+
result = new Date(fieldValue);
137+
} else if (fieldType === 'bytesValue') {
138+
try {
139+
result = Buffer.from(fieldValue, 'base64');
140+
} catch (e) { // Node version < 6, which is the case for Travis CI
141+
result = new Buffer(fieldValue, 'base64');
142+
}
143+
} else if (fieldType === 'referenceValue') {
144+
console.log('WARNING: you have a data field which is a datastore reference. ' +
145+
'There will be a breaking change later which will change it from a string to a reference object.');
146+
result = fieldValue;
147+
} else {
148+
result = fieldValue;
149+
}
150+
});
151+
return result;
152+
}
153+
}

0 commit comments

Comments
 (0)