Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle API JSON format #36

Merged
merged 6 commits into from
Oct 11, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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