Skip to content

Commit

Permalink
Handle API JSON format (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian authored Oct 11, 2017
1 parent 0f35bc8 commit 8a60b4e
Show file tree
Hide file tree
Showing 6 changed files with 650 additions and 188 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"google-gax": "^0.14.1",
"grpc": "^1.6.0",
"is": "^3.2.0",
"safe-buffer": "^5.1.1",
"through2": "^2.0.3"
},
"devDependencies": {
Expand All @@ -66,7 +67,6 @@
"power-assert": "^1.4.4",
"prettier": "^1.7.2",
"proxyquire": "^1.7.11",
"safe-buffer": "^5.1.1",
"typescript": "^2.5.2"
},
"scripts": {
Expand Down
239 changes: 239 additions & 0 deletions src/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*!
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

const Buffer = require('safe-buffer').Buffer;
const is = require('is');

const validate = require('./validate')();

/*!
* @module firestore/convert
* @private
*
* This module contains utility functions to convert
* `firestore.v1beta1.Documents` from Proto3 JSON to their equivalent
* representation in Protobuf JS. Protobuf JS is the only encoding supported by
* this client, and dependencies that use Proto3 JSON (such as the Google Cloud
* Functions SDK) are supported through this conversion and its usage in
* {@see Firestore#snapshot_}.
*/

/**
* Converts an ISO 8601 or google.protobuf.Timestamp proto into Protobuf JS.
*
* @private
* @param {*=} timestampValue - The value to convert.
* @param {string=} argumentName - The argument name to use in the error message
* if the conversion fails. If omitted, 'timestampValue' is used.
* @return {{nanos,seconds}|undefined} The value as expected by Protobuf JS or
* undefined if no input was provided.
*/
function convertTimestamp(timestampValue, argumentName) {
let timestampProto = undefined;

if (is.string(timestampValue)) {
let date = new Date(timestampValue);
let seconds = Math.floor(date.getTime() / 1000);
let nanos = null;

let nanoString = timestampValue.substring(20, timestampValue.length - 1);

if (nanoString.length === 3) {
nanoString = `${nanoString}000000`;
} else if (nanoString.length === 6) {
nanoString = `${nanoString}000`;
}

if (nanoString.length === 9) {
nanos = parseInt(nanoString);
}

if (isNaN(seconds) || isNaN(nanos)) {
argumentName = argumentName || 'timestampValue';
throw new Error(
`Specify a valid ISO 8601 timestamp for "${argumentName}".`
);
}

timestampProto = {
seconds: seconds,
nanos: nanos,
};
} else if (is.defined(timestampValue)) {
validate.isObject('timestampValue', timestampValue);
timestampProto = {
seconds: timestampValue.seconds || 0,
nanos: timestampValue.nanos || 0,
};
}

return timestampProto;
}

/**
* Converts a Proto3 JSON 'bytesValue' field into Protobuf JS.
*
* @private
* @param {*} bytesValue - The value to convert.
* @return {Buffer} The value as expected by Protobuf JS.
*/
function convertBytes(bytesValue) {
if (typeof bytesValue === 'string') {
return Buffer.from(bytesValue, 'base64');
} else {
return bytesValue;
}
}

/**
* Detects 'valueType' from a Proto3 JSON `firestore.v1beta1.Value` proto.
*
* @private
* @param {object} proto - The `firestore.v1beta1.Value` proto.
* @return {string} - The string value for 'valueType'.
*/
function detectValueType(proto) {
if (proto.valueType) {
return proto.valueType;
}

let detectedValues = [];

if (is.defined(proto.stringValue)) {
detectedValues.push('stringValue');
}
if (is.defined(proto.booleanValue)) {
detectedValues.push('booleanValue');
}
if (is.defined(proto.integerValue)) {
detectedValues.push('integerValue');
}
if (is.defined(proto.doubleValue)) {
detectedValues.push('doubleValue');
}
if (is.defined(proto.timestampValue)) {
detectedValues.push('timestampValue');
}
if (is.defined(proto.referenceValue)) {
detectedValues.push('referenceValue');
}
if (is.defined(proto.arrayValue)) {
detectedValues.push('arrayValue');
}
if (is.defined(proto.nullValue)) {
detectedValues.push('nullValue');
}
if (is.defined(proto.mapValue)) {
detectedValues.push('mapValue');
}
if (is.defined(proto.geoPointValue)) {
detectedValues.push('geoPointValue');
}
if (is.defined(proto.bytesValue)) {
detectedValues.push('bytesValue');
}

if (detectedValues.length !== 1) {
throw new Error(
`Unable to infer type value fom '${JSON.stringify(proto)}'.`
);
}

return detectedValues[0];
}

/**
* Converts a `firestore.v1beta1.Value` in Proto3 JSON encoding into the
* Protobuf JS format expected by this client.
*
* @private
* @param {object} fieldValue - The `firestore.v1beta1.Value` in Proto3 JSON
* format.
* @return {object} The `firestore.v1beta1.Value` in Protobuf JS format.
*/
function convertValue(fieldValue) {
let valueType = detectValueType(fieldValue);

switch (valueType) {
case 'timestampValue':
return {
valueType: 'timestampValue',
timestampValue: convertTimestamp(fieldValue.timestampValue),
};
case 'bytesValue':
return {
valueType: 'bytesValue',
bytesValue: convertBytes(fieldValue.bytesValue),
};
case 'arrayValue': {
let arrayValue = [];
for (let value of fieldValue.arrayValue.values) {
arrayValue.push(convertValue(value));
}
return {
valueType: 'arrayValue',
arrayValue: {
values: arrayValue,
},
};
}
case 'mapValue': {
let mapValue = {};
for (let prop in fieldValue.mapValue.fields) {
if (fieldValue.mapValue.fields.hasOwnProperty(prop)) {
mapValue[prop] = convertValue(fieldValue.mapValue.fields[prop]);
}
}
return {
valueType: 'mapValue',
mapValue: {
fields: mapValue,
},
};
}
default:
return Object.assign({valueType}, fieldValue);
}
}

/**
* Converts a `firestore.v1beta1.Document` in Proto3 JSON encoding into the
* Protobuf JS format expected by this client. This conversion creates a copy of
* the underlying document.
*
* @private
* @param {object} document - The `firestore.v1beta1.Document` in Proto3 JSON
* format.
* @return {object} The `firestore.v1beta1.Document` in Protobuf JS format.
*/
function convertDocument(document) {
let result = {};

for (let prop in document) {
if (document.hasOwnProperty(prop)) {
result[prop] = convertValue(document[prop]);
}
}

return result;
}

module.exports = {
documentFromJson: convertDocument,
timestampFromJson: convertTimestamp,
};
36 changes: 6 additions & 30 deletions src/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const assert = require('assert');
const is = require('is');

const path = require('./path');
const timestampFromJson = require('./convert').timestampFromJson;

/*!
* @see {ResourcePath}
Expand Down Expand Up @@ -327,7 +328,7 @@ class DocumentSnapshot {
}

/**
* Returns the underlying Firestore 'Fields' Protobuf.
* Returns the underlying Firestore 'Fields' Protobuf in Protobuf JS format.
*
* @private
* @returns {Object} The Protobuf encoded document.
Expand Down Expand Up @@ -373,7 +374,7 @@ class DocumentSnapshot {
}

/**
* Retrieves the field specified by 'fieldPath' in its Protobuf
* Retrieves the field specified by 'fieldPath' in its Protobuf JS
* representation.
*
* @private
Expand Down Expand Up @@ -1032,35 +1033,10 @@ class Precondition {
let proto = {};

if (is.defined(this._lastUpdateTime)) {
let date = new Date(this._lastUpdateTime);
let seconds = Math.floor(date.getTime() / 1000);
let nanos = null;

let nanoString = this._lastUpdateTime.substring(
20,
this._lastUpdateTime.length - 1
proto.updateTime = timestampFromJson(
this._lastUpdateTime,
'lastUpdateTime'
);

if (nanoString.length === 3) {
nanoString = `${nanoString}000000`;
} else if (nanoString.length === 6) {
nanoString = `${nanoString}000`;
}

if (nanoString.length === 9) {
nanos = parseInt(nanoString);
}

if (isNaN(seconds) || isNaN(nanos)) {
throw new Error(
'Specify a valid ISO 8601 timestamp for' + ' "lastUpdateTime".'
);
}

proto.updateTime = {
seconds: seconds,
nanos: nanos,
};
} else if (is.defined(this._exists)) {
proto.exists = this._exists;
}
Expand Down
Loading

0 comments on commit 8a60b4e

Please sign in to comment.