Merge pull request #1047 from invertase/blob

[firestore] Blob support
This commit is contained in:
Michael Diarmid 2018-05-03 21:03:58 +01:00 committed by GitHub
commit 86a8385f2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 351 additions and 8 deletions

View File

@ -1,14 +1,15 @@
package io.invertase.firebase.firestore; package io.invertase.firebase.firestore;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.google.firebase.firestore.Blob;
import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentChange;
import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.DocumentSnapshot;
@ -18,15 +19,11 @@ import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.GeoPoint; import com.google.firebase.firestore.GeoPoint;
import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.QuerySnapshot;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TimeZone;
import io.invertase.firebase.Utils; import io.invertase.firebase.Utils;
@ -214,6 +211,9 @@ public class FirestoreSerialize {
} else if (value instanceof Date) { } else if (value instanceof Date) {
typeMap.putString("type", "date"); typeMap.putString("type", "date");
typeMap.putDouble("value", ((Date) value).getTime()); typeMap.putDouble("value", ((Date) value).getTime());
} else if (value instanceof Blob) {
typeMap.putString("type", "blob");
typeMap.putString("value", Base64.encodeToString(((Blob) value).toBytes(), Base64.NO_WRAP));
} else { } else {
Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass()); Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass());
typeMap.putString("type", "null"); typeMap.putString("type", "null");
@ -266,6 +266,9 @@ public class FirestoreSerialize {
} else if ("geopoint".equals(type)) { } else if ("geopoint".equals(type)) {
ReadableMap geoPoint = typeMap.getMap("value"); ReadableMap geoPoint = typeMap.getMap("value");
return new GeoPoint(geoPoint.getDouble("latitude"), geoPoint.getDouble("longitude")); return new GeoPoint(geoPoint.getDouble("latitude"), geoPoint.getDouble("longitude"));
} else if ("blob".equals(type)) {
String base64String = typeMap.getString("value");
return Blob.fromBytes(Base64.decode(base64String, Base64.NO_WRAP));
} else if ("date".equals(type)) { } else if ("date".equals(type)) {
Double time = typeMap.getDouble("value"); Double time = typeMap.getDouble("value");
return new Date(time.longValue()); return new Date(time.longValue());

View File

@ -0,0 +1,132 @@
const testObject = { hello: 'world', testRunId };
const testString = JSON.stringify(testObject);
const testBuffer = Buffer.from(testString);
const testBase64 = testBuffer.toString('base64');
const testObjectLarge = new Array(5000).fill(testObject);
const testStringLarge = JSON.stringify(testObjectLarge);
const testBufferLarge = Buffer.from(testStringLarge);
const testBase64Large = testBufferLarge.toString('base64');
// function sizeInKiloBytes(base64String) {
// return 4 * Math.ceil(base64String.length / 3) / 1000;
// }
// console.log(sizeInKiloBytes(testBase64));
// console.log(sizeInKiloBytes(testBase64Large));
/** ----------------
* CLASS TESTS
* -----------------*/
describe('firestore', () => {
it('should export Blob class on statics', async () => {
const { Blob } = firebase.firestore;
should.exist(Blob);
});
describe('Blob', () => {
it('.constructor() -> returns new instance of Blob', async () => {
const { Blob } = firebase.firestore;
const myBlob = new Blob(testStringLarge);
myBlob.should.be.instanceOf(Blob);
myBlob._binaryString.should.equal(testStringLarge);
myBlob.toBase64().should.equal(testBase64Large);
});
it('.fromBase64String() -> returns new instance of Blob', async () => {
const { Blob } = firebase.firestore;
const myBlob = Blob.fromBase64String(testBase64);
myBlob.should.be.instanceOf(Blob);
myBlob._binaryString.should.equal(testString);
should.deepEqual(
JSON.parse(myBlob._binaryString),
testObject,
'Expected Blob _binaryString internals to serialize to json and match test object'
);
});
it('.fromUint8Array() -> returns new instance of Blob', async () => {
const testUInt8Array = new Uint8Array(testBuffer);
const { Blob } = firebase.firestore;
const myBlob = Blob.fromUint8Array(testUInt8Array);
myBlob.should.be.instanceOf(Blob);
const json = JSON.parse(myBlob._binaryString);
json.hello.should.equal('world');
});
});
describe('Blob instance', () => {
it('.toString() -> returns string representation of blob instance', async () => {
const { Blob } = firebase.firestore;
const myBlob = Blob.fromBase64String(testBase64);
myBlob.should.be.instanceOf(Blob);
should.equal(
myBlob.toString().includes(testBase64),
true,
'toString() should return a string that includes the base64'
);
});
it('.toBase64() -> returns base64 string', async () => {
const { Blob } = firebase.firestore;
const myBlob = Blob.fromBase64String(testBase64);
myBlob.should.be.instanceOf(Blob);
myBlob.toBase64().should.equal(testBase64);
});
it('.toUint8Array() -> returns Uint8Array', async () => {
const { Blob } = firebase.firestore;
const myBlob = Blob.fromBase64String(testBase64);
const testUInt8Array = new Uint8Array(testBuffer);
const testUInt8Array2 = new Uint8Array();
myBlob.should.be.instanceOf(Blob);
should.deepEqual(myBlob.toUint8Array(), testUInt8Array);
should.notDeepEqual(myBlob.toUint8Array(), testUInt8Array2);
});
});
});
/** ----------------
* USAGE TESTS
* -----------------*/
describe('firestore', () => {
describe('Blob', () => {
it('reads and writes small blobs', async () => {
const { Blob } = firebase.firestore;
await firebase
.firestore()
.doc('blob-tests/small')
.set({ blobby: Blob.fromBase64String(testBase64) });
const snapshot = await firebase
.firestore()
.doc('blob-tests/small')
.get();
const blob = snapshot.data().blobby;
blob._binaryString.should.equal(testString);
blob.toBase64().should.equal(testBase64);
});
it('reads and writes large blobs', async () => {
const { Blob } = firebase.firestore;
await firebase
.firestore()
.doc('blob-tests/large')
.set({ blobby: Blob.fromBase64String(testBase64Large) });
const snapshot = await firebase
.firestore()
.doc('blob-tests/large')
.get();
const blob = snapshot.data().blobby;
blob._binaryString.should.equal(testStringLarge);
blob.toBase64().should.equal(testBase64Large);
});
});
});

View File

@ -9,6 +9,38 @@ Object.defineProperty(global, 'firebase', {
}, },
}); });
// TODO move as part of bridge
const { Uint8Array } = global;
Object.defineProperty(global, 'Uint8Array', {
get() {
const { stack } = new Error();
if (
(stack.includes('Context.it') || stack.includes('Context.beforeEach')) &&
global.bridge &&
global.bridge.context
) {
return bridge.context.window.Uint8Array;
}
return Uint8Array;
},
});
// TODO move as part of bridge
const { Array } = global;
Object.defineProperty(global, 'Array', {
get() {
const { stack } = new Error();
if (
(stack.includes('Context.it') || stack.includes('Context.beforeEach')) &&
global.bridge &&
global.bridge.context
) {
return bridge.context.window.Array;
}
return Array;
},
});
global.isObject = function isObject(item) { global.isObject = function isObject(item) {
return item return item
? typeof item === 'object' && !Array.isArray(item) && item !== null ? typeof item === 'object' && !Array.isArray(item) && item !== null
@ -30,11 +62,17 @@ global.randomString = (length, chars) => {
} }
return result; return result;
}; };
global.firebaseAdmin = require('firebase-admin');
global.testRunId = randomString(4, 'aA#'); global.testRunId = randomString(4, 'aA#');
/** ------------------
* Init WEB SDK
---------------------*/
/** ------------------
* Init ADMIN SDK
---------------------*/
global.firebaseAdmin = require('firebase-admin');
firebaseAdmin.initializeApp({ firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(require('./service-account')), credential: firebaseAdmin.credential.cert(require('./service-account')),
databaseURL: 'https://rnfirebase-b9ad4.firebaseio.com', databaseURL: 'https://rnfirebase-b9ad4.firebaseio.com',

View File

@ -208,6 +208,10 @@ static NSMutableDictionary *_listeners;
typeMap[@"type"] = @"number"; typeMap[@"type"] = @"number";
} }
typeMap[@"value"] = value; typeMap[@"value"] = value;
} else if ([value isKindOfClass:[NSData class]]) {
typeMap[@"type"] = @"blob";
NSData *blob = (NSData *)value;
typeMap[@"value"] = [blob base64EncodedStringWithOptions:0];
} else { } else {
// TODO: Log an error // TODO: Log an error
typeMap[@"type"] = @"null"; typeMap[@"type"] = @"null";
@ -248,6 +252,8 @@ static NSMutableDictionary *_listeners;
return [RNFirebaseFirestoreDocumentReference parseJSMap:firestore jsMap:value]; return [RNFirebaseFirestoreDocumentReference parseJSMap:firestore jsMap:value];
} else if ([type isEqualToString:@"reference"]) { } else if ([type isEqualToString:@"reference"]) {
return [firestore documentWithPath:value]; return [firestore documentWithPath:value];
} else if ([type isEqualToString:@"blob"]) {
return [[NSData alloc] initWithBase64EncodedString:(NSString *) value options:0];
} else if ([type isEqualToString:@"geopoint"]) { } else if ([type isEqualToString:@"geopoint"]) {
NSDictionary *geopoint = (NSDictionary*)value; NSDictionary *geopoint = (NSDictionary*)value;
NSNumber *latitude = geopoint[@"latitude"]; NSNumber *latitude = geopoint[@"latitude"];

View File

@ -0,0 +1,85 @@
import Base64 from './utils/Base64';
export default class Blob {
_binaryString: string;
constructor(binaryString) {
this._binaryString = binaryString;
}
/**
* Creates a new Blob from the given Base64 string
*
* @url https://firebase.google.com/docs/reference/js/firebase.firestore.Blob#.fromBase64String
* @param base64 string
*/
static fromBase64String(base64: string): Blob {
return new Blob(Base64.atob(base64));
}
/**
* Creates a new Blob from the given Uint8Array.
*
* @url https://firebase.google.com/docs/reference/js/firebase.firestore.Blob#.fromUint8Array
* @param array Array
*/
static fromUint8Array(array: Uint8Array): Blob {
if (!(array instanceof Uint8Array)) {
throw new Error(
'firestore.Blob.fromUint8Array expects an instance of Uint8Array'
);
}
return new Blob(
Array.prototype.map
.call(array, (char: number) => String.fromCharCode(char))
.join('')
);
}
/**
* Returns 'true' if this Blob is equal to the provided one.
* @url https://firebase.google.com/docs/reference/js/firebase.firestore.Blob#isEqual
* @param {*} blob Blob The Blob to compare against. Value must not be null.
* @returns boolean 'true' if this Blob is equal to the provided one.
*/
isEqual(blob: Blob): boolean {
if (!(blob instanceof Blob)) {
throw new Error('firestore.Blob.isEqual expects an instance of Blob');
}
return this._binaryString === blob._binaryString;
}
/**
* Returns the bytes of a Blob as a Base64-encoded string.
*
* @url https://firebase.google.com/docs/reference/js/firebase.firestore.Blob#toBase64
* @returns string The Base64-encoded string created from the Blob object.
*/
toBase64(): string {
return Base64.btoa(this._binaryString);
}
/**
* Returns the bytes of a Blob in a new Uint8Array.
*
* @url https://firebase.google.com/docs/reference/js/firebase.firestore.Blob#toUint8Array
* @returns non-null Uint8Array The Uint8Array created from the Blob object.
*/
toUint8Array(): Uint8Array {
return new Uint8Array(
this._binaryString.split('').map(c => c.charCodeAt(0))
);
}
/**
* Returns a string representation of this blob instance
*
* @returns {string}
* @memberof Blob
*/
toString(): string {
return `firestore.Blob(base64: ${this.toBase64()})`;
}
}

View File

@ -11,6 +11,7 @@ import DocumentReference from './DocumentReference';
import FieldPath from './FieldPath'; import FieldPath from './FieldPath';
import FieldValue from './FieldValue'; import FieldValue from './FieldValue';
import GeoPoint from './GeoPoint'; import GeoPoint from './GeoPoint';
import Blob from './Blob';
import Path from './Path'; import Path from './Path';
import WriteBatch from './WriteBatch'; import WriteBatch from './WriteBatch';
import TransactionHandler from './TransactionHandler'; import TransactionHandler from './TransactionHandler';
@ -258,6 +259,7 @@ export const statics = {
FieldPath, FieldPath,
FieldValue, FieldValue,
GeoPoint, GeoPoint,
Blob,
enableLogging(enabled: boolean): void { enableLogging(enabled: boolean): void {
// DEPRECATED: Remove method in v4.1.0 // DEPRECATED: Remove method in v4.1.0
console.warn( console.warn(

View File

@ -42,6 +42,7 @@ export type NativeTypeMap = {
| 'array' | 'array'
| 'boolean' | 'boolean'
| 'date' | 'date'
| 'blob'
| 'documentid' | 'documentid'
| 'fieldvalue' | 'fieldvalue'
| 'geopoint' | 'geopoint'

View File

@ -0,0 +1,68 @@
// @flow
/* eslint-disable */
const CHARS =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
export default {
/**
* window.btoa
*/
btoa(input: string = ''): string {
let map;
let i = 0;
let block = 0;
let output = '';
// eslint-disable-next-line
for (
block = 0, i = 0, map = CHARS;
input.charAt(i | 0) || ((map = '='), i % 1);
output += map.charAt(63 & (block >> (8 - (i % 1) * 8)))
) {
const charCode = input.charCodeAt((i += 3 / 4));
if (charCode > 0xff) {
throw new Error(
"'firestore.utils.btoa' failed: The string to be encoded contains characters outside of the Latin1 range."
);
}
block = (block << 8) | charCode;
}
return output;
},
/**
* window.atob
*/
atob(input: string = ''): string {
let i = 0;
let bc = 0;
let bs = 0;
let buffer;
let output = '';
const str = input.replace(/=+$/, '');
if (str.length % 4 === 1) {
throw new Error(
"'firestore.utils.atob' failed: The string to be decoded is not correctly encoded."
);
}
// eslint-disable-next-line
for (
bc = 0, bs = 0, i = 0;
(buffer = str.charAt(i++));
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
: 0
) {
buffer = CHARS.indexOf(buffer);
}
return output;
},
};

View File

@ -3,6 +3,7 @@
*/ */
import DocumentReference from '../DocumentReference'; import DocumentReference from '../DocumentReference';
import Blob from '../Blob';
import { DOCUMENT_ID } from '../FieldPath'; import { DOCUMENT_ID } from '../FieldPath';
import { import {
DELETE_FIELD_VALUE, DELETE_FIELD_VALUE,
@ -98,6 +99,11 @@ export const buildTypeMap = (value: any): NativeTypeMap | null => {
type: 'date', type: 'date',
value: value.getTime(), value: value.getTime(),
}; };
} else if (value instanceof Blob) {
return {
type: 'blob',
value: value.toBase64(),
};
} }
return { return {
type: 'object', type: 'object',
@ -156,6 +162,8 @@ const parseTypeMap = (firestore: Firestore, typeMap: NativeTypeMap): any => {
return new GeoPoint(value.latitude, value.longitude); return new GeoPoint(value.latitude, value.longitude);
} else if (type === 'date') { } else if (type === 'date') {
return new Date(value); return new Date(value);
} else if (type === 'blob') {
return Blob.fromBase64String(value);
} }
console.warn(`Unknown data type received ${type}`); console.warn(`Unknown data type received ${type}`);
return value; return value;