commit
86a8385f2a
|
@ -1,14 +1,15 @@
|
|||
package io.invertase.firebase.firestore;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.google.firebase.firestore.Blob;
|
||||
import com.google.firebase.firestore.DocumentChange;
|
||||
import com.google.firebase.firestore.DocumentReference;
|
||||
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.QuerySnapshot;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import io.invertase.firebase.Utils;
|
||||
|
||||
|
@ -214,6 +211,9 @@ public class FirestoreSerialize {
|
|||
} else if (value instanceof Date) {
|
||||
typeMap.putString("type", "date");
|
||||
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 {
|
||||
Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass());
|
||||
typeMap.putString("type", "null");
|
||||
|
@ -266,6 +266,9 @@ public class FirestoreSerialize {
|
|||
} else if ("geopoint".equals(type)) {
|
||||
ReadableMap geoPoint = typeMap.getMap("value");
|
||||
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)) {
|
||||
Double time = typeMap.getDouble("value");
|
||||
return new Date(time.longValue());
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
return item
|
||||
? typeof item === 'object' && !Array.isArray(item) && item !== null
|
||||
|
@ -30,11 +62,17 @@ global.randomString = (length, chars) => {
|
|||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
global.firebaseAdmin = require('firebase-admin');
|
||||
|
||||
global.testRunId = randomString(4, 'aA#');
|
||||
|
||||
/** ------------------
|
||||
* Init WEB SDK
|
||||
---------------------*/
|
||||
|
||||
/** ------------------
|
||||
* Init ADMIN SDK
|
||||
---------------------*/
|
||||
global.firebaseAdmin = require('firebase-admin');
|
||||
|
||||
firebaseAdmin.initializeApp({
|
||||
credential: firebaseAdmin.credential.cert(require('./service-account')),
|
||||
databaseURL: 'https://rnfirebase-b9ad4.firebaseio.com',
|
||||
|
|
|
@ -208,6 +208,10 @@ static NSMutableDictionary *_listeners;
|
|||
typeMap[@"type"] = @"number";
|
||||
}
|
||||
typeMap[@"value"] = value;
|
||||
} else if ([value isKindOfClass:[NSData class]]) {
|
||||
typeMap[@"type"] = @"blob";
|
||||
NSData *blob = (NSData *)value;
|
||||
typeMap[@"value"] = [blob base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
// TODO: Log an error
|
||||
typeMap[@"type"] = @"null";
|
||||
|
@ -248,6 +252,8 @@ static NSMutableDictionary *_listeners;
|
|||
return [RNFirebaseFirestoreDocumentReference parseJSMap:firestore jsMap:value];
|
||||
} else if ([type isEqualToString:@"reference"]) {
|
||||
return [firestore documentWithPath:value];
|
||||
} else if ([type isEqualToString:@"blob"]) {
|
||||
return [[NSData alloc] initWithBase64EncodedString:(NSString *) value options:0];
|
||||
} else if ([type isEqualToString:@"geopoint"]) {
|
||||
NSDictionary *geopoint = (NSDictionary*)value;
|
||||
NSNumber *latitude = geopoint[@"latitude"];
|
||||
|
|
|
@ -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()})`;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import DocumentReference from './DocumentReference';
|
|||
import FieldPath from './FieldPath';
|
||||
import FieldValue from './FieldValue';
|
||||
import GeoPoint from './GeoPoint';
|
||||
import Blob from './Blob';
|
||||
import Path from './Path';
|
||||
import WriteBatch from './WriteBatch';
|
||||
import TransactionHandler from './TransactionHandler';
|
||||
|
@ -258,6 +259,7 @@ export const statics = {
|
|||
FieldPath,
|
||||
FieldValue,
|
||||
GeoPoint,
|
||||
Blob,
|
||||
enableLogging(enabled: boolean): void {
|
||||
// DEPRECATED: Remove method in v4.1.0
|
||||
console.warn(
|
||||
|
|
|
@ -42,6 +42,7 @@ export type NativeTypeMap = {
|
|||
| 'array'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'blob'
|
||||
| 'documentid'
|
||||
| 'fieldvalue'
|
||||
| 'geopoint'
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import DocumentReference from '../DocumentReference';
|
||||
import Blob from '../Blob';
|
||||
import { DOCUMENT_ID } from '../FieldPath';
|
||||
import {
|
||||
DELETE_FIELD_VALUE,
|
||||
|
@ -98,6 +99,11 @@ export const buildTypeMap = (value: any): NativeTypeMap | null => {
|
|||
type: 'date',
|
||||
value: value.getTime(),
|
||||
};
|
||||
} else if (value instanceof Blob) {
|
||||
return {
|
||||
type: 'blob',
|
||||
value: value.toBase64(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'object',
|
||||
|
@ -156,6 +162,8 @@ const parseTypeMap = (firestore: Firestore, typeMap: NativeTypeMap): any => {
|
|||
return new GeoPoint(value.latitude, value.longitude);
|
||||
} else if (type === 'date') {
|
||||
return new Date(value);
|
||||
} else if (type === 'blob') {
|
||||
return Blob.fromBase64String(value);
|
||||
}
|
||||
console.warn(`Unknown data type received ${type}`);
|
||||
return value;
|
||||
|
|
Loading…
Reference in New Issue