From dfd9080281bc98857704842bf9cae3e0a48abb6a Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 26 Sep 2017 14:57:25 +0100 Subject: [PATCH 01/21] [firestore][js] First pass of javascript implementation --- lib/modules/firestore/CollectionReference.js | 105 +++++++++ lib/modules/firestore/DocumentChange.js | 46 ++++ lib/modules/firestore/DocumentReference.js | 122 +++++++++++ lib/modules/firestore/DocumentSnapshot.js | 65 ++++++ lib/modules/firestore/GeoPoint.js | 29 +++ lib/modules/firestore/Path.js | 59 +++++ lib/modules/firestore/Query.js | 214 +++++++++++++++++++ lib/modules/firestore/QuerySnapshot.js | 66 ++++++ lib/modules/firestore/WriteBatch.js | 111 ++++++++++ lib/modules/firestore/index.js | 115 ++++++++++ lib/utils/index.js | 14 ++ 11 files changed, 946 insertions(+) create mode 100644 lib/modules/firestore/CollectionReference.js create mode 100644 lib/modules/firestore/DocumentChange.js create mode 100644 lib/modules/firestore/DocumentReference.js create mode 100644 lib/modules/firestore/DocumentSnapshot.js create mode 100644 lib/modules/firestore/GeoPoint.js create mode 100644 lib/modules/firestore/Path.js create mode 100644 lib/modules/firestore/Query.js create mode 100644 lib/modules/firestore/QuerySnapshot.js create mode 100644 lib/modules/firestore/WriteBatch.js create mode 100644 lib/modules/firestore/index.js diff --git a/lib/modules/firestore/CollectionReference.js b/lib/modules/firestore/CollectionReference.js new file mode 100644 index 00000000..98000ab1 --- /dev/null +++ b/lib/modules/firestore/CollectionReference.js @@ -0,0 +1,105 @@ +/** + * @flow + * CollectionReference representation wrapper + */ +import DocumentReference from './DocumentReference'; +import Path from './Path'; +import Query from './Query'; +import QuerySnapshot from './QuerySnapshot'; +import firestoreAutoId from '../../utils'; + +import type { Direction, Operator } from './Query'; + + /** + * @class CollectionReference + */ +export default class CollectionReference { + _collectionPath: Path; + _firestore: Object; + _query: Query; + + constructor(firestore: Object, collectionPath: Path) { + this._collectionPath = collectionPath; + this._firestore = firestore; + this._query = new Query(firestore, collectionPath); + } + + get firestore(): Object { + return this._firestore; + } + + get id(): string | null { + return this._collectionPath.id; + } + + get parent(): DocumentReference | null { + const parentPath = this._collectionPath.parent(); + return parentPath ? new DocumentReference(this._firestore, parentPath) : null; + } + + add(data: { [string]: any }): Promise { + const documentRef = this.doc(); + return documentRef.set(data) + .then(() => Promise.resolve(documentRef)); + } + + doc(documentPath?: string): DocumentReference { + const newPath = documentPath || firestoreAutoId(); + + const path = this._collectionPath.child(newPath); + if (!path.isDocument) { + throw new Error('Argument "documentPath" must point to a document.'); + } + + return new DocumentReference(this._firestore, path); + } + + // From Query + endAt(fieldValues: any): Query { + return this._query.endAt(fieldValues); + } + + endBefore(fieldValues: any): Query { + return this._query.endBefore(fieldValues); + } + + get(): Promise { + return this._query.get(); + } + + limit(n: number): Query { + return this._query.limit(n); + } + + offset(n: number): Query { + return this._query.offset(n); + } + + onSnapshot(onNext: () => any, onError?: () => any): () => void { + return this._query.onSnapshot(onNext, onError); + } + + orderBy(fieldPath: string, directionStr?: Direction): Query { + return this._query.orderBy(fieldPath, directionStr); + } + + select(varArgs: string[]): Query { + return this._query.select(varArgs); + } + + startAfter(fieldValues: any): Query { + return this._query.startAfter(fieldValues); + } + + startAt(fieldValues: any): Query { + return this._query.startAt(fieldValues); + } + + stream(): Stream { + return this._query.stream(); + } + + where(fieldPath: string, opStr: Operator, value: any): Query { + return this._query.where(fieldPath, opStr, value); + } +} diff --git a/lib/modules/firestore/DocumentChange.js b/lib/modules/firestore/DocumentChange.js new file mode 100644 index 00000000..c9c98b98 --- /dev/null +++ b/lib/modules/firestore/DocumentChange.js @@ -0,0 +1,46 @@ +/** + * @flow + * DocumentChange representation wrapper + */ +import DocumentSnapshot from './DocumentSnapshot'; + + +export type DocumentChangeNativeData = { + document: DocumentSnapshot, + newIndex: number, + oldIndex: number, + type: string, +} + + /** + * @class DocumentChange + */ +export default class DocumentChange { + _document: DocumentSnapshot; + _newIndex: number; + _oldIndex: number; + _type: string; + + constructor(nativeData: DocumentChangeNativeData) { + this._document = nativeData.document; + this._newIndex = nativeData.newIndex; + this._oldIndex = nativeData.oldIndex; + this._type = nativeData.type; + } + + get doc(): DocumentSnapshot { + return this._document; + } + + get newIndex(): number { + return this._newIndex; + } + + get oldIndex(): number { + return this._oldIndex; + } + + get type(): string { + return this._type; + } +} diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js new file mode 100644 index 00000000..53873adf --- /dev/null +++ b/lib/modules/firestore/DocumentReference.js @@ -0,0 +1,122 @@ +/** + * @flow + * DocumentReference representation wrapper + */ +import CollectionReference from './CollectionReference'; +import DocumentSnapshot from './DocumentSnapshot'; +import Path from './Path'; + +export type DeleteOptions = { + lastUpdateTime?: string, +} + +export type UpdateOptions = { + createIfMissing?: boolean, + lastUpdateTime?: string, +} + +export type WriteOptions = { + createIfMissing?: boolean, + lastUpdateTime?: string, +} + +export type WriteResult = { + writeTime: string, +} + + /** + * @class DocumentReference + */ +export default class DocumentReference { + _documentPath: Path; + _firestore: Object; + + constructor(firestore: Object, documentPath: Path) { + this._documentPath = documentPath; + this._firestore = firestore; + } + + get firestore(): Object { + return this._firestore; + } + + get id(): string | null { + return this._documentPath.id; + } + + get parent(): CollectionReference | null { + const parentPath = this._documentPath.parent(); + return parentPath ? new CollectionReference(this._firestore, parentPath) : null; + } + + get path(): string { + return this._documentPath.relativeName; + } + + collection(collectionPath: string): CollectionReference { + const path = this._documentPath.child(collectionPath); + if (!path.isCollection) { + throw new Error('Argument "collectionPath" must point to a collection.'); + } + + return new CollectionReference(this._firestore, path); + } + + create(data: { [string]: any }): Promise { + return this._firestore._native + .documentCreate(this._documentPath._parts, data); + } + + delete(deleteOptions?: DeleteOptions): Promise { + return this._firestore._native + .documentDelete(this._documentPath._parts, deleteOptions); + } + + get(): Promise { + return this._firestore._native + .documentGet(this._documentPath._parts) + .then(result => new DocumentSnapshot(this._firestore, result)); + } + + getCollections(): Promise { + return this._firestore._native + .documentCollections(this._documentPath._parts) + .then((collectionIds) => { + const collections = []; + + for (const collectionId of collectionIds) { + collections.push(this.collection(collectionId)); + } + + return collections; + }); + } + + onSnapshot(onNext: () => any, onError?: () => any): () => void { + // TODO + } + + set(data: { [string]: any }, writeOptions?: WriteOptions): Promise { + return this._firestore._native + .documentSet(this._documentPath._parts, data, writeOptions); + } + + update(data: { [string]: any }, updateOptions?: UpdateOptions): Promise { + return this._firestore._native + .documentUpdate(this._documentPath._parts, data, updateOptions); + } + + /** + * INTERNALS + */ + + /** + * Generate a string that uniquely identifies this DocumentReference + * + * @return {string} + * @private + */ + _getDocumentKey() { + return `$${this._firestore._appName}$/${this._documentPath._parts.join('/')}`; + } +} diff --git a/lib/modules/firestore/DocumentSnapshot.js b/lib/modules/firestore/DocumentSnapshot.js new file mode 100644 index 00000000..a616d97d --- /dev/null +++ b/lib/modules/firestore/DocumentSnapshot.js @@ -0,0 +1,65 @@ +/** + * @flow + * DocumentSnapshot representation wrapper + */ +import DocumentReference from './DocumentReference'; +import Path from './Path'; + +export type DocumentSnapshotNativeData = { + createTime: string, + data: Object, + name: string, + readTime: string, + updateTime: string, +} + +/** + * @class DocumentSnapshot + */ +export default class DocumentSnapshot { + _createTime: string; + _data: Object; + _readTime: string; + _ref: DocumentReference; + _updateTime: string; + + constructor(firestore: Object, nativeData: DocumentSnapshotNativeData) { + this._createTime = nativeData.createTime; + this._data = nativeData.data; + this._ref = new DocumentReference(firestore, Path.fromName(nativeData.name)); + this._readTime = nativeData.readTime; + this._updateTime = nativeData.updateTime; + } + + get createTime(): string { + return this._createTime; + } + + get exists(): boolean { + return this._data !== undefined; + } + + get id(): string | null { + return this._ref.id; + } + + get readTime(): string { + return this._readTime; + } + + get ref(): DocumentReference { + return this._ref; + } + + get updateTime(): string { + return this._updateTime; + } + + data(): Object { + return this._data; + } + + get(fieldPath: string): any { + return this._data[fieldPath]; + } +} diff --git a/lib/modules/firestore/GeoPoint.js b/lib/modules/firestore/GeoPoint.js new file mode 100644 index 00000000..d99cb19d --- /dev/null +++ b/lib/modules/firestore/GeoPoint.js @@ -0,0 +1,29 @@ +/** + * @flow + * GeoPoint representation wrapper + */ + + /** + * @class GeoPoint + */ +export default class GeoPoint { + _latitude: number; + _longitude: number; + + constructor(latitude: number, longitude: number) { + // TODO: Validation + // validate.isNumber('latitude', latitude); + // validate.isNumber('longitude', longitude); + + this._latitude = latitude; + this._longitude = longitude; + } + + get latitude() { + return this._latitude; + } + + get longitude() { + return this._longitude; + } +} diff --git a/lib/modules/firestore/Path.js b/lib/modules/firestore/Path.js new file mode 100644 index 00000000..0c9eb161 --- /dev/null +++ b/lib/modules/firestore/Path.js @@ -0,0 +1,59 @@ +/** + * @flow + * Path representation wrapper + */ + + /** + * @class Path + */ +export default class Path { + _parts: string[]; + + constructor(pathComponents: string[]) { + this._parts = pathComponents; + } + + get id(): string | null { + if (this._parts.length > 0) { + return this._parts[this._parts.length - 1]; + } + return null; + } + + get isDocument(): boolean { + return this._parts.length > 0 && this._parts.length % 2 === 0; + } + + get isCollection(): boolean { + return this._parts.length % 2 === 1; + } + + get relativeName(): string { + return this._parts.join('/'); + } + + child(relativePath: string): Path { + return new Path(this._parts.concat(relativePath.split('/'))); + } + + parent(): Path | null { + if (this._parts.length === 0) { + return null; + } + + return new Path(this._parts.slice(0, this._parts.length - 1)); + } + + /** + * + * @package + */ + static fromName(name): Path { + const parts = name.split('/'); + + if (parts.length === 0) { + return new Path([]); + } + return new Path(parts); + } +} diff --git a/lib/modules/firestore/Query.js b/lib/modules/firestore/Query.js new file mode 100644 index 00000000..c60ba4e9 --- /dev/null +++ b/lib/modules/firestore/Query.js @@ -0,0 +1,214 @@ +/** + * @flow + * Query representation wrapper + */ +import DocumentSnapshot from './DocumentSnapshot'; +import Path from './Path'; +import QuerySnapshot from './QuerySnapshot'; + +const DIRECTIONS = { + DESC: 'descending', + desc: 'descending', + ASC: 'ascending', + asc: 'ascending' +}; +const DOCUMENT_NAME_FIELD = '__name__'; + +const OPERATORS = { + '<': 'LESS_THAN', + '<=': 'LESS_THAN_OR_EQUAL', + '=': 'EQUAL', + '==': 'EQUAL', + '>': 'GREATER_THAN', + '>=': 'GREATER_THAN_OR_EQUAL', +}; + +export type Direction = 'DESC' | 'desc' | 'ASC' | 'asc'; +type FieldFilter = { + fieldPath: string, + operator: string, + value: any, +} +type FieldOrder = { + direction: string, + fieldPath: string, +} +type QueryOptions = { + limit?: number, + offset?: number, + selectFields?: string[], + startAfter?: any[], + startAt?: any[], +} +export type Operator = '<' | '<=' | '=' | '==' | '>' | '>='; + + /** + * @class Query + */ +export default class Query { + _fieldFilters: FieldFilter[]; + _fieldOrders: FieldOrder[]; + _firestore: Object; + _queryOptions: QueryOptions; + _referencePath: Path; + + constructor(firestore: Object, path: Path, fieldFilters?: FieldFilter[], + fieldOrders?: FieldOrder[], queryOptions?: QueryOptions) { + this._fieldFilters = fieldFilters || []; + this._fieldOrders = fieldOrders || []; + this._firestore = firestore; + this._queryOptions = queryOptions || {}; + this._referencePath = path; + } + + get firestore(): Object { + return this._firestore; + } + + endAt(fieldValues: any): Query { + fieldValues = [].slice.call(arguments); + // TODO: Validation + const options = { + ...this._queryOptions, + endAt: fieldValues, + }; + + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + endBefore(fieldValues: any): Query { + fieldValues = [].slice.call(arguments); + // TODO: Validation + const options = { + ...this._queryOptions, + endBefore: fieldValues, + }; + + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + get(): Promise { + return this._firestore._native + .collectionGet( + this._referencePath._parts, + this._fieldFilters, + this._fieldOrders, + this._queryOptions, + ) + .then(nativeData => new QuerySnapshot(nativeData)); + } + + limit(n: number): Query { + // TODO: Validation + // validate.isInteger('n', n); + + const options = { + ...this._queryOptions, + limit: n, + }; + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + offset(n: number): Query { + // TODO: Validation + // validate.isInteger('n', n); + + const options = { + ...this._queryOptions, + offset: n, + }; + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + onSnapshot(onNext: () => any, onError?: () => any): () => void { + + } + + orderBy(fieldPath: string, directionStr?: Direction = 'asc'): Query { + //TODO: Validation + //validate.isFieldPath('fieldPath', fieldPath); + //validate.isOptionalFieldOrder('directionStr', directionStr); + + if (this._queryOptions.startAt || this._queryOptions.endAt) { + throw new Error('Cannot specify an orderBy() constraint after calling ' + + 'startAt(), startAfter(), endBefore() or endAt().'); + } + + const newOrder = { + direction: DIRECTIONS[directionStr], + fieldPath, + }; + const combinedOrders = this._fieldOrders.concat(newOrder); + return new Query(this.firestore, this._referencePath, this._fieldFilters, + combinedOrders, this._queryOptions); + } + + select(varArgs: string[]): Query { + varArgs = Array.isArray(arguments[0]) ? arguments[0] : [].slice.call(arguments); + const fieldReferences = []; + + if (varArgs.length === 0) { + fieldReferences.push(DOCUMENT_NAME_FIELD); + } else { + for (let i = 0; i < varArgs.length; ++i) { + // TODO: Validation + // validate.isFieldPath(i, args[i]); + fieldReferences.push(varArgs[i]); + } + } + + const options = { + ...this._queryOptions, + selectFields: fieldReferences, + }; + + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + startAfter(fieldValues: any): Query { + fieldValues = [].slice.call(arguments); + // TODO: Validation + const options = { + ...this._queryOptions, + startAfter: fieldValues, + }; + + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + startAt(fieldValues: any): Query { + fieldValues = [].slice.call(arguments); + // TODO: Validation + const options = { + ...this._queryOptions, + startAt: fieldValues, + }; + + return new Query(this.firestore, this._referencePath, this._fieldFilters, + this._fieldOrders, options); + } + + stream(): Stream { + + } + + where(fieldPath: string, opStr: Operator, value: any): Query { + // TODO: Validation + // validate.isFieldPath('fieldPath', fieldPath); + // validate.isFieldFilter('fieldFilter', opStr, value); + const newFilter = { + fieldPath, + operator: OPERATORS[opStr], + value, + }; + const combinedFilters = this._fieldFilters.concat(newFilter); + return new Query(this.firestore, this._referencePath, combinedFilters, + this._fieldOrders, this._queryOptions); + } +} diff --git a/lib/modules/firestore/QuerySnapshot.js b/lib/modules/firestore/QuerySnapshot.js new file mode 100644 index 00000000..c1a6b035 --- /dev/null +++ b/lib/modules/firestore/QuerySnapshot.js @@ -0,0 +1,66 @@ +/** + * @flow + * QuerySnapshot representation wrapper + */ +import DocumentChange from './DocumentChange'; +import DocumentSnapshot from './DocumentSnapshot'; +import Query from './Query'; + +import type { DocumentChangeNativeData } from './DocumentChange'; +import type { DocumentSnapshotNativeData } from './DocumentSnapshot'; + +type QuerySnapshotNativeData = { + changes: DocumentChangeNativeData[], + documents: DocumentSnapshotNativeData[], + readTime: string, +} + + /** + * @class QuerySnapshot + */ +export default class QuerySnapshot { + _changes: DocumentChange[]; + _docs: DocumentSnapshot[]; + _query: Query; + _readTime: string; + + constructor(firestore: Object, query: Query, nativeData: QuerySnapshotNativeData) { + this._changes = nativeData.changes.map(change => new DocumentChange(change)); + this._docs = nativeData.documents.map(doc => new DocumentSnapshot(firestore, doc)); + this._query = query; + this._readTime = nativeData.readTime; + } + + get docChanges(): DocumentChange[] { + return this._changes; + } + + get docs(): DocumentSnapshot[] { + return this._docs; + } + + get empty(): boolean { + return this._docs.length === 0; + } + + get query(): Query { + return this._query; + } + + get readTime(): string { + return this._readTime; + } + + get size(): number { + return this._docs.length; + } + + forEach(callback: DocumentSnapshot => any) { + // TODO: Validation + // validate.isFunction('callback', callback); + + for (const doc of this.docs) { + callback(doc); + } + } +} diff --git a/lib/modules/firestore/WriteBatch.js b/lib/modules/firestore/WriteBatch.js new file mode 100644 index 00000000..61ce6979 --- /dev/null +++ b/lib/modules/firestore/WriteBatch.js @@ -0,0 +1,111 @@ +/** + * @flow + * WriteBatch representation wrapper + */ +import DocumentReference from './DocumentReference'; + +import type { DeleteOptions, UpdateOptions, WriteOptions, WriteResult } from './DocumentReference'; + +type CommitOptions = { + transactionId: string, +} + +type DocumentWrite = { + data?: Object, + options?: Object, + path: string[], + type: 'delete' | 'set' | 'update', +} + + /** + * @class WriteBatch + */ +export default class WriteBatch { + _firestore: Object; + _writes: DocumentWrite[]; + + constructor(firestore: Object) { + this._firestore = firestore; + this._writes = []; + } + + get firestore(): Object { + return this._firestore; + } + + get isEmpty(): boolean { + return this._writes.length === 0; + } + + create(docRef: DocumentReference, data: Object): WriteBatch { + // TODO: Validation + // validate.isDocumentReference('docRef', docRef); + // validate.isDocument('data', data); + + return this.set(docRef, data, { exists: false }); + } + + delete(docRef: DocumentReference, deleteOptions?: DeleteOptions): WriteBatch { + // TODO: Validation + // validate.isDocumentReference('docRef', docRef); + // validate.isOptionalPrecondition('deleteOptions', deleteOptions); + this._writes.push({ + options: deleteOptions, + path: docRef._documentPath._parts, + type: 'delete', + }); + + return this; + } + + set(docRef: DocumentReference, data: Object, writeOptions?: WriteOptions) { + // TODO: Validation + // validate.isDocumentReference('docRef', docRef); + // validate.isDocument('data', data); + // validate.isOptionalPrecondition('writeOptions', writeOptions); + + this._writes.push({ + data, + options: writeOptions, + path: docRef._documentPath._parts, + type: 'set', + }); + + // TODO: DocumentTransform ?! + // let documentTransform = DocumentTransform.fromObject(docRef, data); + + // if (!documentTransform.isEmpty) { + // this._writes.push({transform: documentTransform.toProto()}); + // } + + return this; + } + + update(docRef: DocumentReference, data: Object, updateOptions: UpdateOptions): WriteBatch { + // TODO: Validation + // validate.isDocumentReference('docRef', docRef); + // validate.isDocument('data', data, true); + // validate.isOptionalPrecondition('updateOptions', updateOptions); + + this._writes.push({ + data, + options: updateOptions, + path: docRef._documentPath._parts, + type: 'update', + }); + + // TODO: DocumentTransform ?! + // let documentTransform = DocumentTransform.fromObject(docRef, expandedObject); + + // if (!documentTransform.isEmpty) { + // this._writes.push({transform: documentTransform.toProto()}); + // } + + return this; + } + + commit(commitOptions?: CommitOptions): Promise { + return this._firestore._native + .documentBatch(this._writes, commitOptions); + } +} diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js new file mode 100644 index 00000000..740f97b0 --- /dev/null +++ b/lib/modules/firestore/index.js @@ -0,0 +1,115 @@ +/** + * @flow + * Firestore representation wrapper + */ +import { NativeModules } from 'react-native'; + +import ModuleBase from './../../utils/ModuleBase'; +import CollectionReference from './CollectionReference'; +import DocumentReference from './DocumentReference'; +import DocumentSnapshot from './DocumentSnapshot'; +import GeoPoint from './GeoPoint'; +import Path from './Path'; +import WriteBatch from './WriteBatch'; + +const unquotedIdentifier_ = '(?:[A-Za-z_][A-Za-z_0-9]*)'; +const UNQUOTED_IDENTIFIER_REGEX = new RegExp(`^${unquotedIdentifier_}$`); + +/** + * @class Firestore + */ +export default class Firestore extends ModuleBase { + static _NAMESPACE = 'firestore'; + static _NATIVE_MODULE = 'RNFirebaseFirestore'; + + _referencePath: Path; + + constructor(firebaseApp: Object, options: Object = {}) { + super(firebaseApp, options, true); + this._referencePath = new Path([]); + } + + batch(): WriteBatch { + return new WriteBatch(this); + } + + /** + * + * @param collectionPath + * @returns {CollectionReference} + */ + collection(collectionPath: string): CollectionReference { + const path = this._referencePath.child(collectionPath); + if (!path.isCollection) { + throw new Error('Argument "collectionPath" must point to a collection.'); + } + + return new CollectionReference(this, path); + } + + /** + * + * @param documentPath + * @returns {DocumentReference} + */ + doc(documentPath: string): DocumentReference { + const path = this._referencePath.child(documentPath); + if (!path.isDocument) { + throw new Error('Argument "documentPath" must point to a document.'); + } + + return new DocumentReference(this, path); + } + + getAll(varArgs: DocumentReference[]): Promise { + varArgs = Array.isArray(arguments[0]) ? arguments[0] : [].slice.call(arguments); + + const documents = []; + varArgs.forEach((document) => { + // TODO: Validation + // validate.isDocumentReference(i, varArgs[i]); + documents.push(document._documentPath._parts); + }); + return this._native + .documentGetAll(documents) + .then(results => results.map(result => new DocumentSnapshot(this, result))); + } + + getCollections(): Promise { + const rootDocument = new DocumentReference(this, this._referencePath); + return rootDocument.getCollections(); + } + + runTransaction(updateFunction, transactionOptions?: Object): Promise { + + } + + static geoPoint(latitude, longitude): GeoPoint { + return new GeoPoint(latitude, longitude); + } + + static fieldPath(varArgs: string[]): string { + varArgs = Array.isArray(arguments[0]) ? arguments[0] : [].slice.call(arguments); + + let fieldPath = ''; + + for (let i = 0; i < varArgs.length; ++i) { + let component = varArgs[i]; + // TODO: Validation + // validate.isString(i, component); + if (!UNQUOTED_IDENTIFIER_REGEX.test(component)) { + component = `\`${component.replace(/[`\\]/g, '\\$&')} \``; + } + fieldPath += i !== 0 ? `.${component}` : component; + } + + return fieldPath; + } +} + +export const statics = { + FieldValue: { + delete: () => NativeModules.RNFirebaseFirestore && NativeModules.RNFirebaseFirestore.deleteFieldValue || {}, + serverTimestamp: () => NativeModules.RNFirebaseFirestore && NativeModules.RNFirebaseFirestore.serverTimestampFieldValue || {} + }, +}; diff --git a/lib/utils/index.js b/lib/utils/index.js index 09eaea33..254e0983 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -5,6 +5,7 @@ import { Platform } from 'react-native'; // modeled after base64 web-safe chars, but ordered by ASCII const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; +const AUTO_ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const hasOwnProperty = Object.hasOwnProperty; const DEFAULT_CHUNK_SIZE = 50; @@ -373,3 +374,16 @@ export function promiseOrCallback(promise: Promise, optionalCallback?: Function) return Promise.reject(error); }); } + +/** + * Generate a firestore auto id for use with collection/document .add() + * @return {string} + */ +export function firestoreAutoId(): string { + let autoId = ''; + + for (let i = 0; i < 20; i++) { + autoId += AUTO_ID_CHARS.charAt(Math.floor(Math.random() * AUTO_ID_CHARS.length)); + } + return autoId; +} From 52b70d58e392bc80d246ec8c5b11f10dcfe87b87 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 27 Sep 2017 12:57:53 +0100 Subject: [PATCH 02/21] [android] Add the first raft of Android support --- android/build.gradle | 7 +- .../java/io/invertase/firebase/Utils.java | 1 - .../firestore/FirestoreSerialize.java | 195 +++++++++++++++ .../RNFirebaseCollectionReference.java | 136 +++++++++++ .../RNFirebaseDocumentReference.java | 113 +++++++++ .../firestore/RNFirebaseFirestore.java | 223 ++++++++++++++++++ .../firestore/RNFirebaseFirestorePackage.java | 51 ++++ lib/modules/firestore/DocumentReference.js | 34 ++- lib/modules/firestore/DocumentSnapshot.js | 14 +- lib/modules/firestore/Query.js | 34 +-- lib/modules/firestore/WriteBatch.js | 37 +-- lib/modules/firestore/index.js | 8 +- lib/utils/index.js | 18 ++ 13 files changed, 803 insertions(+), 68 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java create mode 100644 android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java create mode 100644 android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java create mode 100644 android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java create mode 100644 android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java diff --git a/android/build.gradle b/android/build.gradle index 7d1f6a3b..ece1ef74 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,6 +33,10 @@ android { allprojects { repositories { jcenter() + mavenLocal() + maven { + url "https://maven.google.com" + } } } @@ -78,11 +82,12 @@ dependencies { compile "com.google.android.gms:play-services-base:$firebaseVersion" compile "com.google.firebase:firebase-core:$firebaseVersion" compile "com.google.firebase:firebase-config:$firebaseVersion" - compile "com.google.firebase:firebase-auth:$firebaseVersion" + compile "com.google.firebase:firebase-auth:11.3.0" compile "com.google.firebase:firebase-database:$firebaseVersion" compile "com.google.firebase:firebase-storage:$firebaseVersion" compile "com.google.firebase:firebase-messaging:$firebaseVersion" compile "com.google.firebase:firebase-crash:$firebaseVersion" compile "com.google.firebase:firebase-perf:$firebaseVersion" compile "com.google.firebase:firebase-ads:$firebaseVersion" + compile 'com.google.firebase:firebase-firestore:11.3.0' } diff --git a/android/src/main/java/io/invertase/firebase/Utils.java b/android/src/main/java/io/invertase/firebase/Utils.java index 15f2d670..69c82a75 100644 --- a/android/src/main/java/io/invertase/firebase/Utils.java +++ b/android/src/main/java/io/invertase/firebase/Utils.java @@ -78,7 +78,6 @@ public class Utils { /** * @param dataSnapshot - * @param registration * @param previousChildName * @return */ diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java new file mode 100644 index 00000000..7fc04663 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -0,0 +1,195 @@ +package io.invertase.firebase.firestore; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.google.firebase.firestore.DocumentChange; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.QuerySnapshot; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class FirestoreSerialize { + private static final String KEY_CHANGES = "changes"; + private static final String KEY_DATA = "data"; + private static final String KEY_DOC_CHANGE_DOCUMENT = "document"; + private static final String KEY_DOC_CHANGE_NEW_INDEX = "newIndex"; + private static final String KEY_DOC_CHANGE_OLD_INDEX = "oldIndex"; + private static final String KEY_DOC_CHANGE_TYPE = "type"; + private static final String KEY_DOCUMENTS = "documents"; + private static final String KEY_PATH = "path"; + + /** + * Convert a DocumentSnapshot instance into a React Native WritableMap + * + * @param documentSnapshot DocumentSnapshot + * @return WritableMap + */ + static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) { + WritableMap documentMap = Arguments.createMap(); + + documentMap.putString(KEY_PATH, documentSnapshot.getId()); + documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); + // Missing fields from web SDK + // createTime + // readTime + // updateTime + + return documentMap; + } + + public static WritableMap snapshotToWritableMap(QuerySnapshot querySnapshot) { + WritableMap queryMap = Arguments.createMap(); + + List documentChanges = querySnapshot.getDocumentChanges(); + if (!documentChanges.isEmpty()) { + queryMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChanges)); + } + + // documents + WritableArray documents = Arguments.createArray(); + List documentSnapshots = querySnapshot.getDocuments(); + for (DocumentSnapshot documentSnapshot : documentSnapshots) { + documents.pushMap(snapshotToWritableMap(documentSnapshot)); + } + queryMap.putArray(KEY_DOCUMENTS, documents); + + return queryMap; + } + + /** + * Convert a List of DocumentChange instances into a React Native WritableArray + * + * @param documentChanges List + * @return WritableArray + */ + static WritableArray documentChangesToWritableArray(List documentChanges) { + WritableArray documentChangesWritable = Arguments.createArray(); + for (DocumentChange documentChange : documentChanges) { + documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange)); + } + return documentChangesWritable; + } + + /** + * Convert a DocumentChange instance into a React Native WritableMap + * + * @param documentChange DocumentChange + * @return WritableMap + */ + static WritableMap documentChangeToWritableMap(DocumentChange documentChange) { + WritableMap documentChangeMap = Arguments.createMap(); + + switch (documentChange.getType()) { + case ADDED: + documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "added"); + break; + case REMOVED: + documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "removed"); + break; + case MODIFIED: + documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "modified"); + } + + documentChangeMap.putMap(KEY_DOC_CHANGE_DOCUMENT, + snapshotToWritableMap(documentChange.getDocument())); + + return documentChangeMap; + } + + /** + * Converts an Object Map into a React Native WritableMap. + * + * @param map Map + * @return WritableMap + */ + static WritableMap objectMapToWritable(Map map) { + WritableMap writableMap = Arguments.createMap(); + for (Map.Entry entry : map.entrySet()) { + putValue(writableMap, entry.getKey(), entry.getValue()); + } + return writableMap; + } + + /** + * Converts an Object array into a React Native WritableArray. + * + * @param array Object[] + * @return WritableArray + */ + static WritableArray objectArrayToWritable(Object[] array) { + WritableArray writableArray = Arguments.createArray(); + + for (Object item : array) { + if (item == null) { + writableArray.pushNull(); + continue; + } + + Class itemClass = item.getClass(); + + if (itemClass == Boolean.class) { + writableArray.pushBoolean((Boolean) item); + } else if (itemClass == Integer.class) { + writableArray.pushDouble(((Integer) item).doubleValue()); + } else if (itemClass == Double.class) { + writableArray.pushDouble((Double) item); + } else if (itemClass == Float.class) { + writableArray.pushDouble(((Float) item).doubleValue()); + } else if (itemClass == String.class) { + writableArray.pushString(item.toString()); + } else if (itemClass == Map.class) { + writableArray.pushMap((objectMapToWritable((Map) item))); + } else if (itemClass == Arrays.class) { + writableArray.pushArray(objectArrayToWritable((Object[]) item)); + } else if (itemClass == List.class) { + List list = (List) item; + Object[] listAsArray = list.toArray(new Object[list.size()]); + writableArray.pushArray(objectArrayToWritable(listAsArray)); + } else { + throw new RuntimeException("Cannot convert object of type " + item); + } + } + + return writableArray; + } + + /** + * Detects an objects type and calls the relevant WritableMap setter method to add the value. + * + * @param map WritableMap + * @param key String + * @param value Object + */ + static void putValue(WritableMap map, String key, Object value) { + if (value == null) { + map.putNull(key); + } else { + Class valueClass = value.getClass(); + + if (valueClass == Boolean.class) { + map.putBoolean(key, (Boolean) value); + } else if (valueClass == Integer.class) { + map.putDouble(key, ((Integer) value).doubleValue()); + } else if (valueClass == Double.class) { + map.putDouble(key, (Double) value); + } else if (valueClass == Float.class) { + map.putDouble(key, ((Float) value).doubleValue()); + } else if (valueClass == String.class) { + map.putString(key, value.toString()); + } else if (valueClass == Map.class) { + map.putMap(key, (objectMapToWritable((Map) value))); + } else if (valueClass == Arrays.class) { + map.putArray(key, objectArrayToWritable((Object[]) value)); + } else if (valueClass == List.class) { + List list = (List) value; + Object[] array = list.toArray(new Object[list.size()]); + map.putArray(key, objectArrayToWritable(array)); + } else { + throw new RuntimeException("Cannot convert object of type " + value); + } + } + } +} diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java new file mode 100644 index 00000000..55dac549 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java @@ -0,0 +1,136 @@ +package io.invertase.firebase.firestore; + + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QuerySnapshot; + +import java.util.List; +import java.util.Map; + +import io.invertase.firebase.Utils; + +public class RNFirebaseCollectionReference { + private static final String TAG = "RNFBCollectionReference"; + private final String appName; + private final String path; + private final ReadableArray filters; + private final ReadableArray orders; + private final ReadableMap options; + private final Query query; + + RNFirebaseCollectionReference(String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options) { + this.appName = appName; + this.path = path; + this.filters = filters; + this.orders = orders; + this.options = options; + this.query = buildQuery(); + } + + void get(final Promise promise) { + query.get().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "get:onComplete:success"); + WritableMap data = FirestoreSerialize.snapshotToWritableMap(task.getResult()); + promise.resolve(data); + } else { + Log.e(TAG, "get:onComplete:failure", task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + } + } + }); + } + + private Query buildQuery() { + Query query = RNFirebaseFirestore.getFirestoreForApp(appName).collection(path); + query = applyFilters(query, filters); + query = applyOrders(query, orders); + query = applyOptions(query, options); + + return query; + } + + private Query applyFilters(Query query, ReadableArray filters) { + List filtersList = Utils.recursivelyDeconstructReadableArray(filters); + + for (Object f : filtersList) { + Map filter = (Map) f; + String fieldPath = (String) filter.get("fieldPath"); + String operator = (String) filter.get("operator"); + Object value = filter.get("value"); + + switch (operator) { + case "EQUAL": + query = query.whereEqualTo(fieldPath, value); + break; + case "GREATER_THAN": + query = query.whereGreaterThan(fieldPath, value); + break; + case "GREATER_THAN_OR_EQUAL": + query = query.whereGreaterThanOrEqualTo(fieldPath, value); + break; + case "LESS_THAN": + query = query.whereLessThan(fieldPath, value); + break; + case "LESS_THAN_OR_EQUAL": + query = query.whereLessThanOrEqualTo(fieldPath, value); + break; + } + } + return query; + } + + private Query applyOrders(Query query, ReadableArray orders) { + List ordersList = Utils.recursivelyDeconstructReadableArray(orders); + for (Object o : ordersList) { + Map order = (Map) o; + String direction = (String) order.get("direction"); + String fieldPath = (String) order.get("fieldPath"); + + query = query.orderBy(fieldPath, Query.Direction.valueOf(direction)); + } + return query; + } + + private Query applyOptions(Query query, ReadableMap options) { + if (options.hasKey("endAt")) { + ReadableArray endAtArray = options.getArray("endAt"); + query = query.endAt(Utils.recursivelyDeconstructReadableArray(endAtArray)); + } + if (options.hasKey("endBefore")) { + ReadableArray endBeforeArray = options.getArray("endBefore"); + query = query.endBefore(Utils.recursivelyDeconstructReadableArray(endBeforeArray)); + } + if (options.hasKey("limit")) { + int limit = options.getInt("limit"); + query = query.limit(limit); + } + if (options.hasKey("offset")) { + // Android doesn't support offset + } + if (options.hasKey("selectFields")) { + // Android doesn't support selectFields + } + if (options.hasKey("startAfter")) { + ReadableArray startAfterArray = options.getArray("startAfter"); + query = query.startAfter(Utils.recursivelyDeconstructReadableArray(startAfterArray)); + } + if (options.hasKey("startAt")) { + ReadableArray startAtArray = options.getArray("startAt"); + query = query.startAt(Utils.recursivelyDeconstructReadableArray(startAtArray)); + } + return query; + } +} diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java new file mode 100644 index 00000000..561e2896 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java @@ -0,0 +1,113 @@ +package io.invertase.firebase.firestore; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.SetOptions; + +import java.util.Map; + +import io.invertase.firebase.Utils; + + +public class RNFirebaseDocumentReference { + private static final String TAG = "RNFBDocumentReference"; + private final String appName; + private final String path; + private final DocumentReference ref; + + RNFirebaseDocumentReference(String appName, String path) { + this.appName = appName; + this.path = path; + this.ref = RNFirebaseFirestore.getFirestoreForApp(appName).document(path); + } + + public void create(ReadableMap data, Promise promise) { + // Not supported on Android out of the box + } + + public void delete(final ReadableMap options, final Promise promise) { + this.ref.delete().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "delete:onComplete:success"); + // Missing fields from web SDK + // writeTime + promise.resolve(Arguments.createMap()); + } else { + Log.e(TAG, "delete:onComplete:failure", task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + } + } + }); + } + + void get(final Promise promise) { + this.ref.get().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "get:onComplete:success"); + WritableMap data = FirestoreSerialize.snapshotToWritableMap(task.getResult()); + promise.resolve(data); + } else { + Log.e(TAG, "get:onComplete:failure", task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + } + } + }); + } + + public void set(final ReadableMap data, final ReadableMap options, final Promise promise) { + Map map = Utils.recursivelyDeconstructReadableMap(data); + SetOptions setOptions = null; + if (options != null && options.hasKey("merge") && options.getBoolean("merge")) { + setOptions = SetOptions.merge(); + } + this.ref.set(map, setOptions).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "set:onComplete:success"); + // Missing fields from web SDK + // writeTime + promise.resolve(Arguments.createMap()); + } else { + Log.e(TAG, "set:onComplete:failure", task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + } + } + }); + } + + public void update(final ReadableMap data, final ReadableMap options, final Promise promise) { + Map map = Utils.recursivelyDeconstructReadableMap(data); + this.ref.update(map).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "update:onComplete:success"); + // Missing fields from web SDK + // writeTime + promise.resolve(Arguments.createMap()); + } else { + Log.e(TAG, "update:onComplete:failure", task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + } + } + }); + } + + public void collections(Promise promise) { + // Not supported on Android out of the box + } +} diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java new file mode 100644 index 00000000..d3fc6883 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -0,0 +1,223 @@ +package io.invertase.firebase.firestore; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.FirebaseApp; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.FieldValue; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.SetOptions; +import com.google.firebase.firestore.WriteBatch; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.invertase.firebase.Utils; + + +public class RNFirebaseFirestore extends ReactContextBaseJavaModule { + private static final String TAG = "RNFirebaseFirestore"; + // private HashMap references = new HashMap<>(); + // private SparseArray transactionHandlers = new SparseArray<>(); + + RNFirebaseFirestore(ReactApplicationContext reactContext) { + super(reactContext); + } + + + /* + * REACT NATIVE METHODS + */ + @ReactMethod + public void collectionGet(String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options, final Promise promise) { + RNFirebaseCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options); + ref.get(promise); + } + + @ReactMethod + public void documentBatch(final String appName, final ReadableArray writes, + final ReadableMap commitOptions, final Promise promise) { + FirebaseFirestore firestore = getFirestoreForApp(appName); + WriteBatch batch = firestore.batch(); + final List writesArray = Utils.recursivelyDeconstructReadableArray(writes); + + for (Object w : writesArray) { + Map write = (Map) w; + String type = (String) write.get("type"); + String path = (String) write.get("path"); + Map data = (Map) write.get("data"); + + DocumentReference ref = firestore.document(path); + switch (type) { + case "DELETE": + batch = batch.delete(ref); + break; + case "SET": + Map options = (Map) write.get("options"); + SetOptions setOptions = null; + if (options != null && options.containsKey("merge") && (boolean)options.get("merge")) { + setOptions = SetOptions.merge(); + } + batch = batch.set(ref, data, setOptions); + break; + case "UPDATE": + batch = batch.update(ref, data); + break; + } + } + + batch.commit().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "set:onComplete:success"); + WritableArray result = Arguments.createArray(); + for (Object w : writesArray) { + // Missing fields from web SDK + // writeTime + result.pushMap(Arguments.createMap()); + } + promise.resolve(result); + } else { + Log.e(TAG, "set:onComplete:failure", task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + } + } + }); + } + + @ReactMethod + public void documentCollections(String appName, String path, final Promise promise) { + RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + ref.collections(promise); + } + + @ReactMethod + public void documentCreate(String appName, String path, ReadableMap data, final Promise promise) { + RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + ref.create(data, promise); + } + + @ReactMethod + public void documentDelete(String appName, String path, ReadableMap options, final Promise promise) { + RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + ref.delete(options, promise); + } + + @ReactMethod + public void documentGet(String appName, String path, final Promise promise) { + RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + ref.get(promise); + } + + @ReactMethod + public void documentGetAll(String appName, ReadableArray documents, final Promise promise) { + // Not supported on Android out of the box + } + + @ReactMethod + public void documentSet(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) { + RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + ref.set(data, options, promise); + } + + @ReactMethod + public void documentUpdate(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) { + RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + ref.update(data, options, promise); + } + + /* + * INTERNALS/UTILS + */ + + /** + * Generates a js-like error from an exception and rejects the provided promise with it. + * + * @param exception Exception Exception normally from a task result. + * @param promise Promise react native promise + */ + static void promiseRejectException(Promise promise, Exception exception) { + // TODO + // WritableMap jsError = getJSError(exception); + promise.reject( + "TODO", // jsError.getString("code"), + exception.getMessage(), // jsError.getString("message"), + exception + ); + } + + /** + * Get a database instance for a specific firebase app instance + * + * @param appName + * @return + */ + static FirebaseFirestore getFirestoreForApp(String appName) { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + return FirebaseFirestore.getInstance(firebaseApp); + } + + /** + * Get a collection reference for a specific app and path + * + * @param appName + * @param filters + * @param orders + * @param options + * @param path @return + */ + private RNFirebaseCollectionReference getCollectionForAppPath(String appName, String path, + ReadableArray filters, + ReadableArray orders, + ReadableMap options) { + return new RNFirebaseCollectionReference(appName, path, filters, orders, options); + } + + /** + * Get a document reference for a specific app and path + * + * @param appName + * @param path + * @return + */ + private RNFirebaseDocumentReference getDocumentForAppPath(String appName, String path) { + return new RNFirebaseDocumentReference(appName, path); + } + + /** + * React Method - returns this module name + * + * @return + */ + @Override + public String getName() { + return "RNFirebaseFirestore"; + } + + /** + * React Native constants for RNFirebaseFirestore + * + * @return + */ + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("deleteFieldValue", FieldValue.delete()); + constants.put("serverTimestampFieldValue", FieldValue.serverTimestamp()); + return constants; + } +} diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java new file mode 100644 index 00000000..528922ac --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java @@ -0,0 +1,51 @@ +package io.invertase.firebase.firestore; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("unused") +public class RNFirebaseFirestorePackage implements ReactPackage { + public RNFirebaseFirestorePackage() { + } + + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNFirebaseFirestore(reactContext)); + + return modules; + } + + /** + * @return list of JS modules to register with the newly created catalyst instance. + *

+ * IMPORTANT: Note that only modules that needs to be accessible from the native code should be + * listed here. Also listing a native module here doesn't imply that the JS implementation of it + * will be automatically included in the JS bundle. + */ + // TODO: Removed in 0.47.0. Here for backwards compatability + public List> createJSModules() { + return Collections.emptyList(); + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js index 53873adf..f569f3d1 100644 --- a/lib/modules/firestore/DocumentReference.js +++ b/lib/modules/firestore/DocumentReference.js @@ -5,19 +5,14 @@ import CollectionReference from './CollectionReference'; import DocumentSnapshot from './DocumentSnapshot'; import Path from './Path'; +import INTERNALS from './../../internals'; export type DeleteOptions = { lastUpdateTime?: string, } -export type UpdateOptions = { - createIfMissing?: boolean, - lastUpdateTime?: string, -} - export type WriteOptions = { - createIfMissing?: boolean, - lastUpdateTime?: string, + merge?: boolean, } export type WriteResult = { @@ -63,24 +58,25 @@ export default class DocumentReference { } create(data: { [string]: any }): Promise { - return this._firestore._native - .documentCreate(this._documentPath._parts, data); + /* return this._firestore._native + .documentCreate(this.path, data); */ + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('DocumentReference', 'create')); } delete(deleteOptions?: DeleteOptions): Promise { return this._firestore._native - .documentDelete(this._documentPath._parts, deleteOptions); + .documentDelete(this.path, deleteOptions); } get(): Promise { return this._firestore._native - .documentGet(this._documentPath._parts) + .documentGet(this.path) .then(result => new DocumentSnapshot(this._firestore, result)); } getCollections(): Promise { - return this._firestore._native - .documentCollections(this._documentPath._parts) + /* return this._firestore._native + .documentCollections(this.path) .then((collectionIds) => { const collections = []; @@ -89,7 +85,8 @@ export default class DocumentReference { } return collections; - }); + }); */ + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('DocumentReference', 'getCollections')); } onSnapshot(onNext: () => any, onError?: () => any): () => void { @@ -98,12 +95,13 @@ export default class DocumentReference { set(data: { [string]: any }, writeOptions?: WriteOptions): Promise { return this._firestore._native - .documentSet(this._documentPath._parts, data, writeOptions); + .documentSet(this.path, data, writeOptions); } - update(data: { [string]: any }, updateOptions?: UpdateOptions): Promise { + // TODO: Update to new update method signature + update(data: { [string]: any }): Promise { return this._firestore._native - .documentUpdate(this._documentPath._parts, data, updateOptions); + .documentUpdate(this.path, data); } /** @@ -117,6 +115,6 @@ export default class DocumentReference { * @private */ _getDocumentKey() { - return `$${this._firestore._appName}$/${this._documentPath._parts.join('/')}`; + return `$${this._firestore._appName}$/${this.path}`; } } diff --git a/lib/modules/firestore/DocumentSnapshot.js b/lib/modules/firestore/DocumentSnapshot.js index a616d97d..f3bc8823 100644 --- a/lib/modules/firestore/DocumentSnapshot.js +++ b/lib/modules/firestore/DocumentSnapshot.js @@ -4,11 +4,12 @@ */ import DocumentReference from './DocumentReference'; import Path from './Path'; +import INTERNALS from './../../internals'; export type DocumentSnapshotNativeData = { createTime: string, data: Object, - name: string, + path: string, readTime: string, updateTime: string, } @@ -26,13 +27,14 @@ export default class DocumentSnapshot { constructor(firestore: Object, nativeData: DocumentSnapshotNativeData) { this._createTime = nativeData.createTime; this._data = nativeData.data; - this._ref = new DocumentReference(firestore, Path.fromName(nativeData.name)); + this._ref = new DocumentReference(firestore, Path.fromName(nativeData.path)); this._readTime = nativeData.readTime; this._updateTime = nativeData.updateTime; } get createTime(): string { - return this._createTime; + // return this._createTime; + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_PROPERTY('DocumentSnapshot', 'createTime')); } get exists(): boolean { @@ -44,7 +46,8 @@ export default class DocumentSnapshot { } get readTime(): string { - return this._readTime; + // return this._readTime; + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_PROPERTY('DocumentSnapshot', 'readTime')); } get ref(): DocumentReference { @@ -52,7 +55,8 @@ export default class DocumentSnapshot { } get updateTime(): string { - return this._updateTime; + // return this._updateTime; + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_PROPERTY('DocumentSnapshot', 'updateTime')); } data(): Object { diff --git a/lib/modules/firestore/Query.js b/lib/modules/firestore/Query.js index c60ba4e9..3d88ca3b 100644 --- a/lib/modules/firestore/Query.js +++ b/lib/modules/firestore/Query.js @@ -5,22 +5,23 @@ import DocumentSnapshot from './DocumentSnapshot'; import Path from './Path'; import QuerySnapshot from './QuerySnapshot'; +import INTERNALS from './../../internals'; const DIRECTIONS = { - DESC: 'descending', - desc: 'descending', - ASC: 'ascending', - asc: 'ascending' + ASC: 'ASCENDING', + asc: 'ASCENDING', + DESC: 'DESCENDING', + desc: 'DESCENDING', }; const DOCUMENT_NAME_FIELD = '__name__'; const OPERATORS = { - '<': 'LESS_THAN', - '<=': 'LESS_THAN_OR_EQUAL', '=': 'EQUAL', '==': 'EQUAL', '>': 'GREATER_THAN', '>=': 'GREATER_THAN_OR_EQUAL', + '<': 'LESS_THAN', + '<=': 'LESS_THAN_OR_EQUAL', }; export type Direction = 'DESC' | 'desc' | 'ASC' | 'asc'; @@ -34,6 +35,8 @@ type FieldOrder = { fieldPath: string, } type QueryOptions = { + endAt?: any[], + endBefore?: any[], limit?: number, offset?: number, selectFields?: string[], @@ -92,12 +95,12 @@ export default class Query { get(): Promise { return this._firestore._native .collectionGet( - this._referencePath._parts, + this._referencePath.relativeName, this._fieldFilters, this._fieldOrders, this._queryOptions, ) - .then(nativeData => new QuerySnapshot(nativeData)); + .then(nativeData => new QuerySnapshot(this._firestore, this, nativeData)); } limit(n: number): Query { @@ -116,12 +119,13 @@ export default class Query { // TODO: Validation // validate.isInteger('n', n); - const options = { + /* const options = { ...this._queryOptions, offset: n, }; return new Query(this.firestore, this._referencePath, this._fieldFilters, - this._fieldOrders, options); + this._fieldOrders, options); */ + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('Query', 'offset')); } onSnapshot(onNext: () => any, onError?: () => any): () => void { @@ -129,9 +133,9 @@ export default class Query { } orderBy(fieldPath: string, directionStr?: Direction = 'asc'): Query { - //TODO: Validation - //validate.isFieldPath('fieldPath', fieldPath); - //validate.isOptionalFieldOrder('directionStr', directionStr); + // TODO: Validation + // validate.isFieldPath('fieldPath', fieldPath); + // validate.isOptionalFieldOrder('directionStr', directionStr); if (this._queryOptions.startAt || this._queryOptions.endAt) { throw new Error('Cannot specify an orderBy() constraint after calling ' + @@ -148,6 +152,7 @@ export default class Query { } select(varArgs: string[]): Query { + /* varArgs = Array.isArray(arguments[0]) ? arguments[0] : [].slice.call(arguments); const fieldReferences = []; @@ -167,7 +172,8 @@ export default class Query { }; return new Query(this.firestore, this._referencePath, this._fieldFilters, - this._fieldOrders, options); + this._fieldOrders, options);*/ + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('Query', 'select')); } startAfter(fieldValues: any): Query { diff --git a/lib/modules/firestore/WriteBatch.js b/lib/modules/firestore/WriteBatch.js index 61ce6979..6fd1fd85 100644 --- a/lib/modules/firestore/WriteBatch.js +++ b/lib/modules/firestore/WriteBatch.js @@ -4,7 +4,7 @@ */ import DocumentReference from './DocumentReference'; -import type { DeleteOptions, UpdateOptions, WriteOptions, WriteResult } from './DocumentReference'; +import type { DeleteOptions, WriteOptions, WriteResult } from './DocumentReference'; type CommitOptions = { transactionId: string, @@ -13,8 +13,8 @@ type CommitOptions = { type DocumentWrite = { data?: Object, options?: Object, - path: string[], - type: 'delete' | 'set' | 'update', + path: string, + type: 'DELETE' | 'SET' | 'UPDATE', } /** @@ -51,8 +51,8 @@ export default class WriteBatch { // validate.isOptionalPrecondition('deleteOptions', deleteOptions); this._writes.push({ options: deleteOptions, - path: docRef._documentPath._parts, - type: 'delete', + path: docRef.path, + type: 'DELETE', }); return this; @@ -67,40 +67,25 @@ export default class WriteBatch { this._writes.push({ data, options: writeOptions, - path: docRef._documentPath._parts, - type: 'set', + path: docRef.path, + type: 'SET', }); - // TODO: DocumentTransform ?! - // let documentTransform = DocumentTransform.fromObject(docRef, data); - - // if (!documentTransform.isEmpty) { - // this._writes.push({transform: documentTransform.toProto()}); - // } - return this; } - update(docRef: DocumentReference, data: Object, updateOptions: UpdateOptions): WriteBatch { + // TODO: Update to new method signature + update(docRef: DocumentReference, data: { [string]: any }): WriteBatch { // TODO: Validation // validate.isDocumentReference('docRef', docRef); // validate.isDocument('data', data, true); - // validate.isOptionalPrecondition('updateOptions', updateOptions); this._writes.push({ data, - options: updateOptions, - path: docRef._documentPath._parts, - type: 'update', + path: docRef.path, + type: 'UPDATE', }); - // TODO: DocumentTransform ?! - // let documentTransform = DocumentTransform.fromObject(docRef, expandedObject); - - // if (!documentTransform.isEmpty) { - // this._writes.push({transform: documentTransform.toProto()}); - // } - return this; } diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js index 740f97b0..b5849ece 100644 --- a/lib/modules/firestore/index.js +++ b/lib/modules/firestore/index.js @@ -11,6 +11,7 @@ import DocumentSnapshot from './DocumentSnapshot'; import GeoPoint from './GeoPoint'; import Path from './Path'; import WriteBatch from './WriteBatch'; +import INTERNALS from './../../internals'; const unquotedIdentifier_ = '(?:[A-Za-z_][A-Za-z_0-9]*)'; const UNQUOTED_IDENTIFIER_REGEX = new RegExp(`^${unquotedIdentifier_}$`); @@ -62,17 +63,18 @@ export default class Firestore extends ModuleBase { } getAll(varArgs: DocumentReference[]): Promise { - varArgs = Array.isArray(arguments[0]) ? arguments[0] : [].slice.call(arguments); + /*varArgs = Array.isArray(arguments[0]) ? arguments[0] : [].slice.call(arguments); const documents = []; varArgs.forEach((document) => { // TODO: Validation // validate.isDocumentReference(i, varArgs[i]); - documents.push(document._documentPath._parts); + documents.push(document.path); }); return this._native .documentGetAll(documents) - .then(results => results.map(result => new DocumentSnapshot(this, result))); + .then(results => results.map(result => new DocumentSnapshot(this, result)));*/ + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD('Query', 'offset')); } getCollections(): Promise { diff --git a/lib/utils/index.js b/lib/utils/index.js index 254e0983..c347dd33 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -9,6 +9,14 @@ const AUTO_ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234 const hasOwnProperty = Object.hasOwnProperty; const DEFAULT_CHUNK_SIZE = 50; +// Source: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical +const REGEXP_FIELD_NAME = new RegExp( + `^(?:\\.?((?:(?:[A-Za-z_][A-Za-z_0-9]*)|(?:[A-Za-z_][A-Za-z_0-9]*))+))$` +); +const REGEXP_FIELD_PATH = new RegExp( + `^((?:(?:[A-Za-z_][A-Za-z_0-9]*)|(?:[A-Za-z_][A-Za-z_0-9]*))+)(?:\\.((?:(?:[A-Za-z_][A-Za-z_0-9]*)|(?:[A-Za-z_][A-Za-z_0-9]*))+))*$` +); + /** * Deep get a value from an object. * @website https://github.com/Salakar/deeps @@ -88,6 +96,16 @@ export function isString(value): Boolean { return typeof value === 'string'; } +/** + * Firestore field name/path validator. + * @param field + * @param paths + * @return {boolean} + */ +export function isValidFirestoreField(field, paths) { + return (paths ? REGEXP_FIELD_PATH : REGEXP_FIELD_NAME).test(field); +} + // platform checks export const isIOS = Platform.OS === 'ios'; export const isAndroid = Platform.OS === 'android'; From bf35c349ae9d2121d2f0a42ceef8de81ce470b0e Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 27 Sep 2017 15:41:25 +0100 Subject: [PATCH 03/21] [firestore][tests] Get first collection tests working on Android --- android/build.gradle | 6 +- .../firestore/FirestoreSerialize.java | 4 +- .../firestore/RNFirebaseFirestore.java | 4 +- lib/firebase-app.js | 2 + lib/firebase.js | 2 + lib/utils/ModuleBase.js | 1 + tests/android/app/build.gradle | 4 +- tests/android/app/google-services.json | 17 ++-- .../MainApplication.java | 2 + tests/android/build.gradle | 5 +- tests/src/tests/firestore/collection/index.js | 30 +++++++ .../tests/firestore/collection/whereTests.js | 86 +++++++++++++++++++ tests/src/tests/firestore/document/index.js | 22 +++++ tests/src/tests/firestore/index.js | 19 ++++ tests/src/tests/index.js | 40 +++++---- 15 files changed, 206 insertions(+), 38 deletions(-) create mode 100644 tests/src/tests/firestore/collection/index.js create mode 100644 tests/src/tests/firestore/collection/whereTests.js create mode 100644 tests/src/tests/firestore/document/index.js create mode 100644 tests/src/tests/firestore/index.js diff --git a/android/build.gradle b/android/build.gradle index ece1ef74..7010bfdd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.firebaseVersion = '11.2.0' + ext.firebaseVersion = '11.3.0' repositories { jcenter() } @@ -82,12 +82,12 @@ dependencies { compile "com.google.android.gms:play-services-base:$firebaseVersion" compile "com.google.firebase:firebase-core:$firebaseVersion" compile "com.google.firebase:firebase-config:$firebaseVersion" - compile "com.google.firebase:firebase-auth:11.3.0" + compile "com.google.firebase:firebase-auth:$firebaseVersion" compile "com.google.firebase:firebase-database:$firebaseVersion" compile "com.google.firebase:firebase-storage:$firebaseVersion" compile "com.google.firebase:firebase-messaging:$firebaseVersion" compile "com.google.firebase:firebase-crash:$firebaseVersion" compile "com.google.firebase:firebase-perf:$firebaseVersion" compile "com.google.firebase:firebase-ads:$firebaseVersion" - compile 'com.google.firebase:firebase-firestore:11.3.0' + compile "com.google.firebase:firebase-firestore:$firebaseVersion" } diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index 7fc04663..3bcb84ec 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -44,9 +44,7 @@ public class FirestoreSerialize { WritableMap queryMap = Arguments.createMap(); List documentChanges = querySnapshot.getDocumentChanges(); - if (!documentChanges.isEmpty()) { - queryMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChanges)); - } + queryMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChanges)); // documents WritableArray documents = Arguments.createArray(); diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index d3fc6883..382f1708 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -216,8 +216,8 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { @Override public Map getConstants() { final Map constants = new HashMap<>(); - constants.put("deleteFieldValue", FieldValue.delete()); - constants.put("serverTimestampFieldValue", FieldValue.serverTimestamp()); + constants.put("deleteFieldValue", FieldValue.delete().toString()); + constants.put("serverTimestampFieldValue", FieldValue.serverTimestamp().toString()); return constants; } } diff --git a/lib/firebase-app.js b/lib/firebase-app.js index 7e48bb24..f4eb69e4 100644 --- a/lib/firebase-app.js +++ b/lib/firebase-app.js @@ -12,6 +12,7 @@ import RemoteConfig from './modules/config'; import Storage, { statics as StorageStatics } from './modules/storage'; import Database, { statics as DatabaseStatics } from './modules/database'; import Messaging, { statics as MessagingStatics } from './modules/messaging'; +import Firestore, { statics as FirestoreStatics } from './modules/firestore'; const FirebaseCoreModule = NativeModules.RNFirebase; @@ -32,6 +33,7 @@ export default class FirebaseApp { this.config = this._staticsOrModuleInstance({}, RemoteConfig); this.crash = this._staticsOrModuleInstance({}, Crash); this.database = this._staticsOrModuleInstance(DatabaseStatics, Database); + this.firestore = this._staticsOrModuleInstance(FirestoreStatics, Firestore); this.messaging = this._staticsOrModuleInstance(MessagingStatics, Messaging); this.perf = this._staticsOrModuleInstance({}, Performance); this.storage = this._staticsOrModuleInstance(StorageStatics, Storage); diff --git a/lib/firebase.js b/lib/firebase.js index b31352f5..c67b4f8f 100644 --- a/lib/firebase.js +++ b/lib/firebase.js @@ -20,6 +20,7 @@ import RemoteConfig from './modules/config'; import Storage, { statics as StorageStatics } from './modules/storage'; import Database, { statics as DatabaseStatics } from './modules/database'; import Messaging, { statics as MessagingStatics } from './modules/messaging'; +import Firestore, { statics as FirestoreStatics } from './modules/firestore'; const FirebaseCoreModule = NativeModules.RNFirebase; @@ -47,6 +48,7 @@ class FirebaseCore { this.config = this._appNamespaceOrStatics({}, RemoteConfig); this.crash = this._appNamespaceOrStatics({}, Crash); this.database = this._appNamespaceOrStatics(DatabaseStatics, Database); + this.firestore = this._appNamespaceOrStatics(FirestoreStatics, Firestore); this.messaging = this._appNamespaceOrStatics(MessagingStatics, Messaging); this.perf = this._appNamespaceOrStatics(DatabaseStatics, Performance); this.storage = this._appNamespaceOrStatics(StorageStatics, Storage); diff --git a/lib/utils/ModuleBase.js b/lib/utils/ModuleBase.js index d2c58cf5..127050f9 100644 --- a/lib/utils/ModuleBase.js +++ b/lib/utils/ModuleBase.js @@ -13,6 +13,7 @@ const logs = {}; const MULTI_APP_MODULES = [ 'auth', 'database', + 'firestore', 'storage', ]; diff --git a/tests/android/app/build.gradle b/tests/android/app/build.gradle index 347d6988..ec316968 100644 --- a/tests/android/app/build.gradle +++ b/tests/android/app/build.gradle @@ -71,7 +71,7 @@ android { } } -project.ext.firebaseVersion = '11.2.0' +project.ext.firebaseVersion = '11.3.0' dependencies { // compile(project(':react-native-firebase')) { @@ -82,7 +82,6 @@ dependencies { compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.google.android.gms:play-services-base:$firebaseVersion" compile "com.google.firebase:firebase-ads:$firebaseVersion" - compile "com.google.firebase:firebase-ads:$firebaseVersion" compile "com.google.firebase:firebase-auth:$firebaseVersion" compile "com.google.firebase:firebase-config:$firebaseVersion" compile "com.google.firebase:firebase-core:$firebaseVersion" @@ -91,6 +90,7 @@ dependencies { compile "com.google.firebase:firebase-messaging:$firebaseVersion" compile "com.google.firebase:firebase-perf:$firebaseVersion" compile "com.google.firebase:firebase-storage:$firebaseVersion" + compile "com.google.firebase:firebase-firestore:$firebaseVersion" compile "com.android.support:appcompat-v7:26.0.1" compile "com.facebook.react:react-native:+" // From node_modules } diff --git a/tests/android/app/google-services.json b/tests/android/app/google-services.json index 30a94c5d..35ae2548 100644 --- a/tests/android/app/google-services.json +++ b/tests/android/app/google-services.json @@ -1,27 +1,30 @@ { "project_info": { - "project_number": "305229645282", - "firebase_url": "https://rnfirebase-b9ad4.firebaseio.com", - "project_id": "rnfirebase-b9ad4", - "storage_bucket": "rnfirebase-b9ad4.appspot.com" + "project_number": "17067372085", + "firebase_url": "https://rnfirebase-5579a.firebaseio.com", + "project_id": "rnfirebase", + "storage_bucket": "rnfirebase.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:305229645282:android:efe37851d57e1d05", + "mobilesdk_app_id": "1:17067372085:android:efe37851d57e1d05", "android_client_info": { "package_name": "com.reactnativefirebasedemo" } }, "oauth_client": [ { - "client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com", + "client_id": "17067372085-n572o9802h9jbv9oo60h53117pk9333k.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyCzbBYFyX8d6VdSu7T4s10IWYbPc-dguwM" + "current_key": "AIzaSyB-z0ytgXRRiClvslJl0tp-KbhDub9o6AM" + }, + { + "current_key": "AIzaSyAJw8mR1fPcEYC9ouZbkCStJufcCQrhmjQ" } ], "services": { diff --git a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java index 68af9279..a9ec77f6 100644 --- a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java +++ b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java @@ -10,6 +10,7 @@ import io.invertase.firebase.auth.RNFirebaseAuthPackage; import io.invertase.firebase.config.RNFirebaseRemoteConfigPackage; import io.invertase.firebase.crash.RNFirebaseCrashPackage; import io.invertase.firebase.database.RNFirebaseDatabasePackage; +import io.invertase.firebase.firestore.RNFirebaseFirestorePackage; import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; import io.invertase.firebase.perf.RNFirebasePerformancePackage; import io.invertase.firebase.storage.RNFirebaseStoragePackage; @@ -42,6 +43,7 @@ public class MainApplication extends Application implements ReactApplication { new RNFirebaseRemoteConfigPackage(), new RNFirebaseCrashPackage(), new RNFirebaseDatabasePackage(), + new RNFirebaseFirestorePackage(), new RNFirebaseMessagingPackage(), new RNFirebasePerformancePackage(), new RNFirebaseStoragePackage() diff --git a/tests/android/build.gradle b/tests/android/build.gradle index 59621ffe..32ba1492 100644 --- a/tests/android/build.gradle +++ b/tests/android/build.gradle @@ -6,8 +6,8 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' - classpath 'com.google.gms:google-services:3.1.0' - classpath 'com.google.firebase:firebase-plugins:1.1.0' + classpath 'com.google.gms:google-services:3.1.1' + classpath 'com.google.firebase:firebase-plugins:1.1.1' } } @@ -18,6 +18,7 @@ allprojects { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$rootDir/../node_modules/react-native/android" } + mavenLocal() google() } } diff --git a/tests/src/tests/firestore/collection/index.js b/tests/src/tests/firestore/collection/index.js new file mode 100644 index 00000000..d46d4e9e --- /dev/null +++ b/tests/src/tests/firestore/collection/index.js @@ -0,0 +1,30 @@ +// import docSnapTests from './docSnapTests'; +// import querySnapTests from './querySnapTests'; +// import onSnapshotTests from './onSnapshotTests'; +// import bugTests from './bugTests'; +import whereTests from './whereTests'; + +const testGroups = [ + // onSnapshotTests, + // querySnapTests, + // docSnapTests, + // bugTests, + whereTests, +]; + +function registerTestSuite(testSuite) { + testSuite.beforeEach(async function () { + // todo reset test data + }); + + testSuite.afterEach(async function () { + // todo reset test data + }); + + testGroups.forEach((testGroup) => { + testGroup(testSuite); + }); +} + + +module.exports = registerTestSuite; diff --git a/tests/src/tests/firestore/collection/whereTests.js b/tests/src/tests/firestore/collection/whereTests.js new file mode 100644 index 00000000..9dfe6da7 --- /dev/null +++ b/tests/src/tests/firestore/collection/whereTests.js @@ -0,0 +1,86 @@ +import should from 'should'; + +/** + +Test document structure from fb console: + + baz: true + daz: 123 + foo: "bar" + gaz: 12.1234567 + naz: null + +*/ + +function whereTests({ describe, it, context, firebase }) { + describe('CollectionReference.where()', () => { + context('correctly handles', () => { + it('== boolean values', () => { + return firebase.native.firestore() + .collection('tests') + .where('baz', '==', true) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().baz, true); + }); + }); + }); + + it('== string values', () => { + return firebase.native.firestore() + .collection('tests') + .where('foo', '==', 'bar') + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().foo, 'bar'); + }); + }); + }); + + it('== null values', () => { + return firebase.native.firestore() + .collection('tests') + .where('naz', '==', null) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().naz, null); + }); + }); + }); + + it('>= number values', () => { + return firebase.native.firestore() + .collection('tests') + .where('daz', '>=', 123) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().daz, 123); + }); + }); + }); + + it('<= float values', () => { + return firebase.native.firestore() + .collection('tests') + .where('gaz', '<=', 12.1234666) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().gaz, 12.1234567); + }); + }); + }); + }); + }); +} + +export default whereTests; diff --git a/tests/src/tests/firestore/document/index.js b/tests/src/tests/firestore/document/index.js new file mode 100644 index 00000000..d6949789 --- /dev/null +++ b/tests/src/tests/firestore/document/index.js @@ -0,0 +1,22 @@ +// import whereTests from './whereTests'; + +const testGroups = [ + // whereTests, +]; + +function registerTestSuite(testSuite) { + testSuite.beforeEach(async function () { + // todo reset test data + }); + + testSuite.afterEach(async function () { + // todo reset test data + }); + + testGroups.forEach((testGroup) => { + testGroup(testSuite); + }); +} + + +module.exports = registerTestSuite; diff --git a/tests/src/tests/firestore/index.js b/tests/src/tests/firestore/index.js new file mode 100644 index 00000000..20990491 --- /dev/null +++ b/tests/src/tests/firestore/index.js @@ -0,0 +1,19 @@ +import firebase from '../../firebase'; +import TestSuite from '../../../lib/TestSuite'; + +/* + Test suite files + */ +import collectionTestGroups from './collection/index'; +import documentTestGroups from './document/index'; + +const suite = new TestSuite('Firestore', 'firebase.firestore()', firebase); + +/* + Register tests with test suite + */ + +suite.addTests(documentTestGroups); +suite.addTests(collectionTestGroups); + +export default suite; diff --git a/tests/src/tests/index.js b/tests/src/tests/index.js index 85d1a390..5c4d2bf2 100644 --- a/tests/src/tests/index.js +++ b/tests/src/tests/index.js @@ -1,26 +1,28 @@ import { setSuiteStatus, setTestStatus } from '../actions/TestActions'; -import analytics from './analytics/index'; -import crash from './crash/index'; -import core from './core/index'; -import database from './database/index'; -import messaging from './messaging/index'; -import storage from './storage/index'; -import auth from './auth/index'; -import config from './config/index'; -import performance from './perf/index'; -import admob from './admob/index'; +import analytics from './analytics'; +import crash from './crash'; +import core from './core'; +import database from './database'; +import messaging from './messaging'; +import storage from './storage'; +import auth from './auth'; +import config from './config'; +import performance from './perf'; +import admob from './admob'; +import firestore from './firestore'; const testSuiteInstances = [ - database, - auth, - analytics, - messaging, - crash, - core, - storage, - config, - performance, admob, + analytics, + auth, + config, + core, + crash, + database, + firestore, + messaging, + performance, + storage, ]; /* From 867a08da7b7f826965fa4234cf8cf5fcfa97f3da Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 27 Sep 2017 17:20:32 +0100 Subject: [PATCH 04/21] [firestore][android] Resolve a few issues with basic operations --- .../firebase/firestore/FirestoreSerialize.java | 2 +- .../firestore/RNFirebaseDocumentReference.java | 9 ++++++--- .../firebase/firestore/RNFirebaseFirestore.java | 11 ++++++----- lib/modules/firestore/CollectionReference.js | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index 3bcb84ec..8bdbc7e3 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -30,7 +30,7 @@ public class FirestoreSerialize { static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) { WritableMap documentMap = Arguments.createMap(); - documentMap.putString(KEY_PATH, documentSnapshot.getId()); + documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath()); documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); // Missing fields from web SDK // createTime diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java index 561e2896..fe0f7219 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java @@ -69,11 +69,14 @@ public class RNFirebaseDocumentReference { public void set(final ReadableMap data, final ReadableMap options, final Promise promise) { Map map = Utils.recursivelyDeconstructReadableMap(data); + Task task; SetOptions setOptions = null; if (options != null && options.hasKey("merge") && options.getBoolean("merge")) { - setOptions = SetOptions.merge(); + task = this.ref.set(map, SetOptions.merge()); + } else { + task = this.ref.set(map); } - this.ref.set(map, setOptions).addOnCompleteListener(new OnCompleteListener() { + task.addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { if (task.isSuccessful()) { @@ -89,7 +92,7 @@ public class RNFirebaseDocumentReference { }); } - public void update(final ReadableMap data, final ReadableMap options, final Promise promise) { + public void update(final ReadableMap data, final Promise promise) { Map map = Utils.recursivelyDeconstructReadableMap(data); this.ref.update(map).addOnCompleteListener(new OnCompleteListener() { @Override diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index 382f1708..12c4bc99 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -67,11 +67,12 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { break; case "SET": Map options = (Map) write.get("options"); - SetOptions setOptions = null; if (options != null && options.containsKey("merge") && (boolean)options.get("merge")) { - setOptions = SetOptions.merge(); + batch = batch.set(ref, data, SetOptions.merge()); + } else { + batch = batch.set(ref, data); } - batch = batch.set(ref, data, setOptions); + break; case "UPDATE": batch = batch.update(ref, data); @@ -135,9 +136,9 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { } @ReactMethod - public void documentUpdate(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) { + public void documentUpdate(String appName, String path, ReadableMap data, final Promise promise) { RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); - ref.update(data, options, promise); + ref.update(data, promise); } /* diff --git a/lib/modules/firestore/CollectionReference.js b/lib/modules/firestore/CollectionReference.js index 98000ab1..3c53be1d 100644 --- a/lib/modules/firestore/CollectionReference.js +++ b/lib/modules/firestore/CollectionReference.js @@ -6,7 +6,7 @@ import DocumentReference from './DocumentReference'; import Path from './Path'; import Query from './Query'; import QuerySnapshot from './QuerySnapshot'; -import firestoreAutoId from '../../utils'; +import { firestoreAutoId } from '../../utils'; import type { Direction, Operator } from './Query'; From 8ac16931a65a526a6244bf6c122a8026e5087edb Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Wed, 27 Sep 2017 17:25:20 +0100 Subject: [PATCH 05/21] [firestore][android] Rename a couple of classes --- .../firestore/RNFirebaseFirestore.java | 28 +++++++++---------- ...FirebaseFirestoreCollectionReference.java} | 8 +++--- ...RNFirebaseFirestoreDocumentReference.java} | 6 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) rename android/src/main/java/io/invertase/firebase/firestore/{RNFirebaseCollectionReference.java => RNFirebaseFirestoreCollectionReference.java} (93%) rename android/src/main/java/io/invertase/firebase/firestore/{RNFirebaseDocumentReference.java => RNFirebaseFirestoreDocumentReference.java} (95%) diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index 12c4bc99..968f5403 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -43,7 +43,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { @ReactMethod public void collectionGet(String appName, String path, ReadableArray filters, ReadableArray orders, ReadableMap options, final Promise promise) { - RNFirebaseCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options); + RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options); ref.get(promise); } @@ -102,25 +102,25 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { @ReactMethod public void documentCollections(String appName, String path, final Promise promise) { - RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.collections(promise); } @ReactMethod public void documentCreate(String appName, String path, ReadableMap data, final Promise promise) { - RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.create(data, promise); } @ReactMethod public void documentDelete(String appName, String path, ReadableMap options, final Promise promise) { - RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.delete(options, promise); } @ReactMethod public void documentGet(String appName, String path, final Promise promise) { - RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.get(promise); } @@ -131,13 +131,13 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { @ReactMethod public void documentSet(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) { - RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.set(data, options, promise); } @ReactMethod public void documentUpdate(String appName, String path, ReadableMap data, final Promise promise) { - RNFirebaseDocumentReference ref = getDocumentForAppPath(appName, path); + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.update(data, promise); } @@ -181,11 +181,11 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { * @param options * @param path @return */ - private RNFirebaseCollectionReference getCollectionForAppPath(String appName, String path, - ReadableArray filters, - ReadableArray orders, - ReadableMap options) { - return new RNFirebaseCollectionReference(appName, path, filters, orders, options); + private RNFirebaseFirestoreCollectionReference getCollectionForAppPath(String appName, String path, + ReadableArray filters, + ReadableArray orders, + ReadableMap options) { + return new RNFirebaseFirestoreCollectionReference(appName, path, filters, orders, options); } /** @@ -195,8 +195,8 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { * @param path * @return */ - private RNFirebaseDocumentReference getDocumentForAppPath(String appName, String path) { - return new RNFirebaseDocumentReference(appName, path); + private RNFirebaseFirestoreDocumentReference getDocumentForAppPath(String appName, String path) { + return new RNFirebaseFirestoreDocumentReference(appName, path); } /** diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java similarity index 93% rename from android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java rename to android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java index 55dac549..1609df79 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseCollectionReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java @@ -18,8 +18,8 @@ import java.util.Map; import io.invertase.firebase.Utils; -public class RNFirebaseCollectionReference { - private static final String TAG = "RNFBCollectionReference"; +public class RNFirebaseFirestoreCollectionReference { + private static final String TAG = "RNFSCollectionReference"; private final String appName; private final String path; private final ReadableArray filters; @@ -27,8 +27,8 @@ public class RNFirebaseCollectionReference { private final ReadableMap options; private final Query query; - RNFirebaseCollectionReference(String appName, String path, ReadableArray filters, - ReadableArray orders, ReadableMap options) { + RNFirebaseFirestoreCollectionReference(String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options) { this.appName = appName; this.path = path; this.filters = filters; diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java similarity index 95% rename from android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java rename to android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java index fe0f7219..2b3a494e 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseDocumentReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java @@ -18,13 +18,13 @@ import java.util.Map; import io.invertase.firebase.Utils; -public class RNFirebaseDocumentReference { - private static final String TAG = "RNFBDocumentReference"; +public class RNFirebaseFirestoreDocumentReference { + private static final String TAG = "RNFBFSDocumentReference"; private final String appName; private final String path; private final DocumentReference ref; - RNFirebaseDocumentReference(String appName, String path) { + RNFirebaseFirestoreDocumentReference(String appName, String path) { this.appName = appName; this.path = path; this.ref = RNFirebaseFirestore.getFirestoreForApp(appName).document(path); From f56435226d9331e2fff6127e92b6269868aa4639 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 28 Sep 2017 13:46:33 +0100 Subject: [PATCH 06/21] [firestore][android] Couple of fixes --- .../firebase/firestore/FirestoreSerialize.java | 2 ++ .../RNFirebaseFirestoreCollectionReference.java | 12 ++++++------ .../RNFirebaseFirestoreDocumentReference.java | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index 8bdbc7e3..b196a07d 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -93,6 +93,8 @@ public class FirestoreSerialize { documentChangeMap.putMap(KEY_DOC_CHANGE_DOCUMENT, snapshotToWritableMap(documentChange.getDocument())); + documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex()); + documentChangeMap.putInt(KEY_DOC_CHANGE_OLD_INDEX, documentChange.getOldIndex()); return documentChangeMap; } diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java index 1609df79..9f41ab6a 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java @@ -55,14 +55,14 @@ public class RNFirebaseFirestoreCollectionReference { private Query buildQuery() { Query query = RNFirebaseFirestore.getFirestoreForApp(appName).collection(path); - query = applyFilters(query, filters); - query = applyOrders(query, orders); - query = applyOptions(query, options); + query = applyFilters(query); + query = applyOrders(query); + query = applyOptions(query); return query; } - private Query applyFilters(Query query, ReadableArray filters) { + private Query applyFilters(Query query) { List filtersList = Utils.recursivelyDeconstructReadableArray(filters); for (Object f : filtersList) { @@ -92,7 +92,7 @@ public class RNFirebaseFirestoreCollectionReference { return query; } - private Query applyOrders(Query query, ReadableArray orders) { + private Query applyOrders(Query query) { List ordersList = Utils.recursivelyDeconstructReadableArray(orders); for (Object o : ordersList) { Map order = (Map) o; @@ -104,7 +104,7 @@ public class RNFirebaseFirestoreCollectionReference { return query; } - private Query applyOptions(Query query, ReadableMap options) { + private Query applyOptions(Query query) { if (options.hasKey("endAt")) { ReadableArray endAtArray = options.getArray("endAt"); query = query.endAt(Utils.recursivelyDeconstructReadableArray(endAtArray)); diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java index 2b3a494e..6d5bfed1 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java @@ -30,6 +30,10 @@ public class RNFirebaseFirestoreDocumentReference { this.ref = RNFirebaseFirestore.getFirestoreForApp(appName).document(path); } + public void collections(Promise promise) { + // Not supported on Android + } + public void create(ReadableMap data, Promise promise) { // Not supported on Android out of the box } @@ -109,8 +113,4 @@ public class RNFirebaseFirestoreDocumentReference { } }); } - - public void collections(Promise promise) { - // Not supported on Android out of the box - } } From 6060c36c1c8aca5c1c91e55df51e4d8b0441ee87 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 28 Sep 2017 13:48:28 +0100 Subject: [PATCH 07/21] [firestore][ios] Add initial iOS functionality --- ios/RNFirebase.xcodeproj/project.pbxproj | 27 +++ .../firestore/RNFirebaseFirestore.h | 26 +++ .../firestore/RNFirebaseFirestore.m | 162 ++++++++++++++++++ .../RNFirebaseFirestoreCollectionReference.h | 30 ++++ .../RNFirebaseFirestoreCollectionReference.m | 145 ++++++++++++++++ .../RNFirebaseFirestoreDocumentReference.h | 32 ++++ .../RNFirebaseFirestoreDocumentReference.m | 100 +++++++++++ lib/modules/firestore/Query.js | 1 - tests/ios/GoogleService-Info.plist | 28 +-- tests/ios/Podfile | 1 + tests/ios/Podfile.lock | 40 ++++- tests/src/main.firestore.js | 106 ++++++++++++ 12 files changed, 682 insertions(+), 16 deletions(-) create mode 100644 ios/RNFirebase/firestore/RNFirebaseFirestore.h create mode 100644 ios/RNFirebase/firestore/RNFirebaseFirestore.m create mode 100644 ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h create mode 100644 ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m create mode 100644 ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h create mode 100644 ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m create mode 100644 tests/src/main.firestore.js diff --git a/ios/RNFirebase.xcodeproj/project.pbxproj b/ios/RNFirebase.xcodeproj/project.pbxproj index 5d91af3c..ed509361 100644 --- a/ios/RNFirebase.xcodeproj/project.pbxproj +++ b/ios/RNFirebase.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 8323CF071F6FBD870071420B /* NativeExpressComponent.m in Sources */ = {isa = PBXBuildFile; fileRef = 8323CF011F6FBD870071420B /* NativeExpressComponent.m */; }; 8323CF081F6FBD870071420B /* RNFirebaseAdMobBannerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8323CF031F6FBD870071420B /* RNFirebaseAdMobBannerManager.m */; }; 8323CF091F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8323CF051F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.m */; }; + 8376F7141F7C149100D45A85 /* RNFirebaseFirestoreDocumentReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F70E1F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.m */; }; + 8376F7151F7C149100D45A85 /* RNFirebaseFirestore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F7101F7C149000D45A85 /* RNFirebaseFirestore.m */; }; + 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */; }; 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D914F1EF3E20A0077C7C8 /* RNFirebaseAdMob.m */; }; 839D916D1EF3E20B0077C7C8 /* RNFirebaseAdMobInterstitial.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91511EF3E20A0077C7C8 /* RNFirebaseAdMobInterstitial.m */; }; 839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 839D91531EF3E20A0077C7C8 /* RNFirebaseAdMobRewardedVideo.m */; }; @@ -50,6 +53,12 @@ 8323CF031F6FBD870071420B /* RNFirebaseAdMobBannerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseAdMobBannerManager.m; sourceTree = ""; }; 8323CF041F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMobNativeExpressManager.h; sourceTree = ""; }; 8323CF051F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseAdMobNativeExpressManager.m; sourceTree = ""; }; + 8376F70E1F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseFirestoreDocumentReference.m; sourceTree = ""; }; + 8376F70F1F7C149000D45A85 /* RNFirebaseFirestore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestore.h; sourceTree = ""; }; + 8376F7101F7C149000D45A85 /* RNFirebaseFirestore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseFirestore.m; sourceTree = ""; }; + 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseFirestoreCollectionReference.m; sourceTree = ""; }; + 8376F7121F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestoreDocumentReference.h; sourceTree = ""; }; + 8376F7131F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFirestoreCollectionReference.h; sourceTree = ""; }; 839D914E1EF3E20A0077C7C8 /* RNFirebaseAdMob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMob.h; sourceTree = ""; }; 839D914F1EF3E20A0077C7C8 /* RNFirebaseAdMob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseAdMob.m; sourceTree = ""; }; 839D91501EF3E20A0077C7C8 /* RNFirebaseAdMobInterstitial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseAdMobInterstitial.h; sourceTree = ""; }; @@ -105,6 +114,7 @@ 839D915A1EF3E20A0077C7C8 /* config */, 839D915D1EF3E20A0077C7C8 /* crash */, 839D91601EF3E20A0077C7C8 /* database */, + 8376F70D1F7C141500D45A85 /* firestore */, 839D91631EF3E20A0077C7C8 /* messaging */, 839D91661EF3E20A0077C7C8 /* perf */, 839D91691EF3E20A0077C7C8 /* storage */, @@ -115,6 +125,20 @@ ); sourceTree = ""; }; + 8376F70D1F7C141500D45A85 /* firestore */ = { + isa = PBXGroup; + children = ( + 8376F70F1F7C149000D45A85 /* RNFirebaseFirestore.h */, + 8376F7101F7C149000D45A85 /* RNFirebaseFirestore.m */, + 8376F7131F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.h */, + 8376F7111F7C149000D45A85 /* RNFirebaseFirestoreCollectionReference.m */, + 8376F7121F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.h */, + 8376F70E1F7C149000D45A85 /* RNFirebaseFirestoreDocumentReference.m */, + ); + name = firestore; + path = RNFirebase/firestore; + sourceTree = ""; + }; 839D914D1EF3E20A0077C7C8 /* admob */ = { isa = PBXGroup; children = ( @@ -277,9 +301,12 @@ files = ( 839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */, 839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */, + 8376F7161F7C149100D45A85 /* RNFirebaseFirestoreCollectionReference.m in Sources */, 839D91761EF3E20B0077C7C8 /* RNFirebaseStorage.m in Sources */, + 8376F7151F7C149100D45A85 /* RNFirebaseFirestore.m in Sources */, 839D91701EF3E20B0077C7C8 /* RNFirebaseAuth.m in Sources */, 8323CF091F6FBD870071420B /* RNFirebaseAdMobNativeExpressManager.m in Sources */, + 8376F7141F7C149100D45A85 /* RNFirebaseFirestoreDocumentReference.m in Sources */, 839D916F1EF3E20B0077C7C8 /* RNFirebaseAnalytics.m in Sources */, 839D91711EF3E20B0077C7C8 /* RNFirebaseRemoteConfig.m in Sources */, D950369E1D19C77400F7094D /* RNFirebase.m in Sources */, diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.h b/ios/RNFirebase/firestore/RNFirebaseFirestore.h new file mode 100644 index 00000000..af556283 --- /dev/null +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.h @@ -0,0 +1,26 @@ +#ifndef RNFirebaseFirestore_h +#define RNFirebaseFirestore_h + +#import + +#if __has_include() + +#import +#import +#import + +@interface RNFirebaseFirestore : RCTEventEmitter {} + ++ (void)promiseRejectException:(RCTPromiseRejectBlock)reject error:(NSError *)error; + ++ (FIRFirestore *)getFirestoreForApp:(NSString *)appName; + +@end + +#else +@interface RNFirebaseFirestore : NSObject +@end +#endif + +#endif + diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.m b/ios/RNFirebase/firestore/RNFirebaseFirestore.m new file mode 100644 index 00000000..c15a894f --- /dev/null +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.m @@ -0,0 +1,162 @@ +#import "RNFirebaseFirestore.h" + +#if __has_include() + +#import +#import "RNFirebaseEvents.h" +#import "RNFirebaseFirestoreCollectionReference.h" +#import "RNFirebaseFirestoreDocumentReference.h" + +@implementation RNFirebaseFirestore +RCT_EXPORT_MODULE(); + +- (id)init { + self = [super init]; + if (self != nil) { + + } + return self; +} + +RCT_EXPORT_METHOD(collectionGet:(NSString *) appName + path:(NSString *) path + filters:(NSArray *) filters + orders:(NSArray *) orders + options:(NSDictionary *) options + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getCollectionForAppPath:appName path:path filters:filters orders:orders options:options] get:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(documentBatch:(NSString *) appName + writes:(NSArray *) writes + commitOptions:(NSDictionary *) commitOptions + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + FIRFirestore *firestore = [RNFirebaseFirestore getFirestoreForApp:appName]; + FIRWriteBatch *batch = [firestore batch]; + + for (NSDictionary *write in writes) { + NSString *type = write[@"type"]; + NSString *path = write[@"path"]; + NSDictionary *data = write[@"data"]; + + FIRDocumentReference *ref = [firestore documentWithPath:path]; + + if ([type isEqualToString:@"DELETE"]) { + batch = [batch deleteDocument:ref]; + } else if ([type isEqualToString:@"SET"]) { + NSDictionary *options = write[@"options"]; + if (options && options[@"merge"]) { + batch = [batch setData:data forDocument:ref options:[FIRSetOptions merge]]; + } else { + batch = [batch setData:data forDocument:ref]; + } + } else if ([type isEqualToString:@"UPDATE"]) { + batch = [batch updateData:data forDocument:ref]; + } + } + + [batch commitWithCompletion:^(NSError * _Nullable error) { + if (error) { + [RNFirebaseFirestore promiseRejectException:reject error:error]; + } else { + NSMutableArray *result = [[NSMutableArray alloc] init]; + for (NSDictionary *write in writes) { + // Missing fields from web SDK + // writeTime + [result addObject:@{}]; + } + resolve(result); + } + }]; +} + +RCT_EXPORT_METHOD(documentCollections:(NSString *) appName + path:(NSString *) path + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getDocumentForAppPath:appName path:path] get:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(documentCreate:(NSString *) appName + path:(NSString *) path + data:(NSDictionary *) data + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getDocumentForAppPath:appName path:path] create:data resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(documentDelete:(NSString *) appName + path:(NSString *) path + options:(NSDictionary *) options + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getDocumentForAppPath:appName path:path] delete:options resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(documentGet:(NSString *) appName + path:(NSString *) path + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getDocumentForAppPath:appName path:path] get:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(documentGetAll:(NSString *) appName + documents:(NSString *) documents + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + // Not supported on iOS out of the box +} + +RCT_EXPORT_METHOD(documentSet:(NSString *) appName + path:(NSString *) path + data:(NSDictionary *) data + options:(NSDictionary *) options + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getDocumentForAppPath:appName path:path] set:data options:options resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName + path:(NSString *) path + data:(NSDictionary *) data + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject) { + [[self getDocumentForAppPath:appName path:path] update:data resolver:resolve rejecter:reject]; +} + +/* + * INTERNALS/UTILS + */ ++ (void)promiseRejectException:(RCTPromiseRejectBlock)reject error:(NSError *)error { + // TODO + // NSDictionary *jsError = [RNFirebaseDatabase getJSError:databaseError]; + // reject([jsError valueForKey:@"code"], [jsError valueForKey:@"message"], databaseError); + reject(@"TODO", [error description], error); +} + ++ (FIRFirestore *)getFirestoreForApp:(NSString *)appName { + FIRApp *app = [FIRApp appNamed:appName]; + return [FIRFirestore firestoreForApp:app]; +} + +- (RNFirebaseFirestoreCollectionReference *)getCollectionForAppPath:(NSString *)appName path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options { + return [[RNFirebaseFirestoreCollectionReference alloc] initWithPathAndModifiers:appName path:path filters:filters orders:orders options:options]; +} + +- (RNFirebaseFirestoreDocumentReference *)getDocumentForAppPath:(NSString *)appName path:(NSString *)path { + return [[RNFirebaseFirestoreDocumentReference alloc] initWithPath:appName path:path]; +} + +- (NSArray *)supportedEvents { + return @[DATABASE_SYNC_EVENT, DATABASE_TRANSACTION_EVENT]; +} + +@end + +#else +@implementation RNFirebaseFirestore +@end +#endif + diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h new file mode 100644 index 00000000..04c668dd --- /dev/null +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h @@ -0,0 +1,30 @@ +#ifndef RNFirebaseFirestoreCollectionReference_h +#define RNFirebaseFirestoreCollectionReference_h +#import + +#if __has_include() + +#import +#import "RNFirebaseFirestore.h" +#import "RNFirebaseFirestoreDocumentReference.h" + +@interface RNFirebaseFirestoreCollectionReference : NSObject +@property NSString *app; +@property NSString *path; +@property NSArray *filters; +@property NSArray *orders; +@property NSDictionary *options; +@property FIRQuery *query; + +- (id)initWithPathAndModifiers:(NSString *)app path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options; +- (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; ++ (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot; +@end + +#else + +@interface RNFirebaseFirestoreCollectionReference : NSObject +@end +#endif + +#endif diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m new file mode 100644 index 00000000..1b558963 --- /dev/null +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m @@ -0,0 +1,145 @@ +#import "RNFirebaseFirestoreCollectionReference.h" + +@implementation RNFirebaseFirestoreCollectionReference + +#if __has_include() + +- (id)initWithPathAndModifiers:(NSString *) app + path:(NSString *) path + filters:(NSArray *) filters + orders:(NSArray *) orders + options:(NSDictionary *) options { + self = [super init]; + if (self) { + _app = app; + _path = path; + _filters = filters; + _orders = orders; + _options = options; + _query = [self buildQuery]; + } + return self; +} + +- (void)get:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + [_query getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) { + if (error) { + [RNFirebaseFirestore promiseRejectException:reject error:error]; + } else { + NSDictionary *data = [RNFirebaseFirestoreCollectionReference snapshotToDictionary:snapshot]; + resolve(data); + } + }]; +} + +- (FIRQuery *)buildQuery { + FIRQuery *query = (FIRQuery*)[[RNFirebaseFirestore getFirestoreForApp:_app] collectionWithPath:_path]; + query = [self applyFilters:query]; + query = [self applyOrders:query]; + query = [self applyOptions:query]; + + return query; +} + +- (FIRQuery *)applyFilters:(FIRQuery *) query { + for (NSDictionary *filter in _filters) { + NSString *fieldPath = filter[@"fieldPath"]; + NSString *operator = filter[@"operator"]; + // TODO: Validate this works + id value = filter[@"value"]; + + if ([operator isEqualToString:@"EQUAL"]) { + query = [query queryWhereField:fieldPath isEqualTo:value]; + } else if ([operator isEqualToString:@"GREATER_THAN"]) { + query = [query queryWhereField:fieldPath isGreaterThan:value]; + } else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) { + query = [query queryWhereField:fieldPath isGreaterThanOrEqualTo:value]; + } else if ([operator isEqualToString:@"LESS_THAN"]) { + query = [query queryWhereField:fieldPath isLessThan:value]; + } else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) { + query = [query queryWhereField:fieldPath isLessThanOrEqualTo:value]; + } + } + return query; +} + +- (FIRQuery *)applyOrders:(FIRQuery *) query { + for (NSDictionary *order in _orders) { + NSString *direction = order[@"direction"]; + NSString *fieldPath = order[@"fieldPath"]; + + query = [query queryOrderedByField:fieldPath descending:([direction isEqualToString:@"DESCENDING"])]; + } + return query; +} + +- (FIRQuery *)applyOptions:(FIRQuery *) query { + if (_options[@"endAt"]) { + query = [query queryEndingAtValues:_options[@"endAt"]]; + } + if (_options[@"endBefore"]) { + query = [query queryEndingBeforeValues:_options[@"endBefore"]]; + } + if (_options[@"offset"]) { + // iOS doesn't support offset + } + if (_options[@"selectFields"]) { + // iOS doesn't support selectFields + } + if (_options[@"startAfter"]) { + query = [query queryStartingAfterValues:_options[@"startAfter"]]; + } + if (_options[@"startAt"]) { + query = [query queryStartingAtValues:_options[@"startAt"]]; + } + return query; +} + ++ (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot { + NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; + [snapshot setValue:[self documentChangesToArray:querySnapshot.documentChanges] forKey:@"changes"]; + [snapshot setValue:[self documentSnapshotsToArray:querySnapshot.documents] forKey:@"documents"]; + + return snapshot; +} + ++ (NSArray *)documentChangesToArray:(NSArray *) documentChanges { + NSMutableArray *changes = [[NSMutableArray alloc] init]; + for (FIRDocumentChange *change in documentChanges) { + [changes addObject:[self documentChangeToDictionary:change]]; + } + + return changes; +} + ++ (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange { + NSMutableDictionary *change = [[NSMutableDictionary alloc] init]; + [change setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentChange.document] forKey:@"document"]; + [change setValue:@(documentChange.newIndex) forKey:@"newIndex"]; + [change setValue:@(documentChange.oldIndex) forKey:@"oldIndex"]; + + if (documentChange.type == FIRDocumentChangeTypeAdded) { + [change setValue:@"added" forKey:@"type"]; + } else if (documentChange.type == FIRDocumentChangeTypeRemoved) { + [change setValue:@"removed" forKey:@"type"]; + } else if (documentChange.type == FIRDocumentChangeTypeModified) { + [change setValue:@"modified" forKey:@"type"]; + } + + return change; +} + ++ (NSArray *)documentSnapshotsToArray:(NSArray *) documentSnapshots { + NSMutableArray *snapshots = [[NSMutableArray alloc] init]; + for (FIRDocumentSnapshot *snapshot in documentSnapshots) { + [snapshots addObject:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]]; + } + + return snapshots; +} + +#endif + +@end + diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h new file mode 100644 index 00000000..b4113c2c --- /dev/null +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h @@ -0,0 +1,32 @@ +#ifndef RNFirebaseFirestoreDocumentReference_h +#define RNFirebaseFirestoreDocumentReference_h + +#import + +#if __has_include() + +#import +#import "RNFirebaseFirestore.h" + +@interface RNFirebaseFirestoreDocumentReference : NSObject +@property NSString *app; +@property NSString *path; +@property FIRDocumentReference *ref; + +- (id)initWithPath:(NSString *)app path:(NSString *)path; +- (void)collections:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (void)create:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (void)delete:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (void)set:(NSDictionary *)data options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (void)update:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; ++ (NSDictionary *)snapshotToDictionary:(FIRDocumentSnapshot *)documentSnapshot; +@end + +#else + +@interface RNFirebaseFirestoreDocumentReference : NSObject +@end +#endif + +#endif diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m new file mode 100644 index 00000000..8b1b1bbb --- /dev/null +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -0,0 +1,100 @@ +#import "RNFirebaseFirestoreDocumentReference.h" + +@implementation RNFirebaseFirestoreDocumentReference + +#if __has_include() + +- (id)initWithPath:(NSString *) app + path:(NSString *) path { + self = [super init]; + if (self) { + _app = app; + _path = path; + _ref = [[RNFirebaseFirestore getFirestoreForApp:_app] documentWithPath:_path]; + } + return self; +} + +- (void)collections:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + // Not supported on iOS +} + +- (void)create:(NSDictionary *) data + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + // Not supported on iOS out of the box +} + +- (void)delete:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + [_ref deleteDocumentWithCompletion:^(NSError * _Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; +} + +- (void)get:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + [_ref getDocumentWithCompletion:^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) { + if (error) { + [RNFirebaseFirestore promiseRejectException:reject error:error]; + } else { + NSDictionary *data = [RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]; + resolve(data); + } + }]; +} + +- (void)set:(NSDictionary *) data + options:(NSDictionary *) options + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + if (options && options[@"merge"]) { + [_ref setData:data options:[FIRSetOptions merge] completion:^(NSError * _Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; + } else { + [_ref setData:data completion:^(NSError * _Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; + } +} + +- (void)update:(NSDictionary *) data + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + [_ref updateData:data completion:^(NSError * _Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; +} + ++ (void)handleWriteResponse:(NSError *) error + resolver:(RCTPromiseResolveBlock) resolve + rejecter:(RCTPromiseRejectBlock) reject { + if (error) { + [RNFirebaseFirestore promiseRejectException:reject error:error]; + } else { + // Missing fields from web SDK + // writeTime + resolve(@{}); + } +} + ++ (NSDictionary *)snapshotToDictionary:(FIRDocumentSnapshot *)documentSnapshot { + NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; + [snapshot setValue:documentSnapshot.reference.path forKey:@"path"]; + [snapshot setValue:documentSnapshot.data forKey:@"data"]; + // Missing fields from web SDK + // createTime + // readTime + // updateTime + + return snapshot; +} + +#endif + +@end + + diff --git a/lib/modules/firestore/Query.js b/lib/modules/firestore/Query.js index 3d88ca3b..4ec64817 100644 --- a/lib/modules/firestore/Query.js +++ b/lib/modules/firestore/Query.js @@ -13,7 +13,6 @@ const DIRECTIONS = { DESC: 'DESCENDING', desc: 'DESCENDING', }; -const DOCUMENT_NAME_FIELD = '__name__'; const OPERATORS = { '=': 'EQUAL', diff --git a/tests/ios/GoogleService-Info.plist b/tests/ios/GoogleService-Info.plist index 30da4b8d..079738ad 100644 --- a/tests/ios/GoogleService-Info.plist +++ b/tests/ios/GoogleService-Info.plist @@ -7,34 +7,34 @@ AD_UNIT_ID_FOR_INTERSTITIAL_TEST ca-app-pub-3940256099942544/4411468910 CLIENT_ID - 305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib.apps.googleusercontent.com + 17067372085-h95lq6v2fbjdl2i1f6pl26iurah37i8p.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib + com.googleusercontent.apps.17067372085-h95lq6v2fbjdl2i1f6pl26iurah37i8p API_KEY - AIzaSyAcdVLG5dRzA1ck_fa_xd4Z0cY7cga7S5A + AIzaSyC8ZEruBCvS_6woF8_l07ILy1eXaD6J4vQ GCM_SENDER_ID - 305229645282 + 17067372085 PLIST_VERSION 1 BUNDLE_ID com.invertase.ReactNativeFirebaseDemo PROJECT_ID - rnfirebase-b9ad4 + rnfirebase STORAGE_BUCKET - rnfirebase-b9ad4.appspot.com + rnfirebase.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:305229645282:ios:7b45748cb1117d2d + 1:17067372085:ios:7b45748cb1117d2d DATABASE_URL - https://rnfirebase-b9ad4.firebaseio.com + https://rnfirebase-5579a.firebaseio.com - \ No newline at end of file + diff --git a/tests/ios/Podfile b/tests/ios/Podfile index 2f3300b0..af1fb7a7 100644 --- a/tests/ios/Podfile +++ b/tests/ios/Podfile @@ -19,6 +19,7 @@ target 'ReactNativeFirebaseDemo' do pod 'Firebase/Crash' pod 'Firebase/Database' pod 'Firebase/DynamicLinks' + pod 'Firestore', :podspec => 'https://storage.googleapis.com/firebase-preview-drop/ios/firestore/0.7.0/Firestore.podspec.json' pod 'Firebase/Messaging' pod 'Firebase/RemoteConfig' pod 'Firebase/Storage' diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index ebb97db9..7d2c3986 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -1,4 +1,10 @@ PODS: + - BoringSSL (8.2): + - BoringSSL/Implementation (= 8.2) + - BoringSSL/Interface (= 8.2) + - BoringSSL/Implementation (8.2): + - BoringSSL/Interface (= 8.2) + - BoringSSL/Interface (8.2) - Firebase/AdMob (4.1.0): - Firebase/Core - Google-Mobile-Ads-SDK (= 7.22.0) @@ -77,6 +83,13 @@ PODS: - FirebaseAnalytics (~> 4.0) - FirebaseCore (~> 4.0) - GTMSessionFetcher/Core (~> 1.1) + - Firestore (0.7.0): + - FirebaseAnalytics (~> 4.0) + - FirebaseAuth (~> 4.1) + - FirebaseCore (~> 4.0) + - gRPC-ProtoRPC (~> 1.0) + - leveldb-library (~> 1.18) + - Protobuf (~> 3.1) - Google-Mobile-Ads-SDK (7.22.0) - GoogleToolboxForMac/DebugUtils (2.1.1): - GoogleToolboxForMac/Defines (= 2.1.1) @@ -90,6 +103,22 @@ PODS: - GoogleToolboxForMac/Defines (= 2.1.1) - GoogleToolboxForMac/NSString+URLArguments (= 2.1.1) - GoogleToolboxForMac/NSString+URLArguments (2.1.1) + - gRPC (1.4.2): + - gRPC-Core (= 1.4.2) + - gRPC-RxLibrary (= 1.4.2) + - gRPC-Core (1.4.2): + - gRPC-Core/Implementation (= 1.4.2) + - gRPC-Core/Interface (= 1.4.2) + - gRPC-Core/Implementation (1.4.2): + - BoringSSL (~> 8.0) + - gRPC-Core/Interface (= 1.4.2) + - nanopb (~> 0.3) + - gRPC-Core/Interface (1.4.2) + - gRPC-ProtoRPC (1.4.2): + - gRPC (= 1.4.2) + - gRPC-RxLibrary (= 1.4.2) + - Protobuf (~> 3.0) + - gRPC-RxLibrary (1.4.2) - GTMSessionFetcher/Core (1.1.11) - leveldb-library (1.18.3) - nanopb (0.3.8): @@ -121,11 +150,14 @@ DEPENDENCIES: - Firebase/Performance - Firebase/RemoteConfig - Firebase/Storage + - Firestore (from `https://storage.googleapis.com/firebase-preview-drop/ios/firestore/0.7.0/Firestore.podspec.json`) - React (from `../node_modules/react-native`) - RNFirebase (from `./../../`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) EXTERNAL SOURCES: + Firestore: + :podspec: https://storage.googleapis.com/firebase-preview-drop/ios/firestore/0.7.0/Firestore.podspec.json React: :path: "../node_modules/react-native" RNFirebase: @@ -134,6 +166,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: + BoringSSL: 4135ae556ee2b82ee85477c39ba917a3dd5424ba Firebase: ebebf41db7f10e0c7668b6eaaa857fbe599aa478 FirebaseAnalytics: 76f754d37ca5b04f36856729b6af3ca0152d1069 FirebaseAuth: 8d1d2389cf82f891048d6d50d27d044f55ae09a6 @@ -146,8 +179,13 @@ SPEC CHECKSUMS: FirebasePerformance: 36bdb0500213b459ae991766801d5dc5399ff231 FirebaseRemoteConfig: 5b3e3301ef2f237b1b588e8ef3211b5a22e9e15d FirebaseStorage: 661fc1f8d4131891d256b62e82a45ace8b3f0c3b + Firestore: 80f352a0b5260500b11d7e4626b81a19d4eba312 Google-Mobile-Ads-SDK: 1bdf1a4244d0553b1840239874c209c01aef055f GoogleToolboxForMac: 8e329f1b599f2512c6b10676d45736bcc2cbbeb0 + gRPC: 74b57d3c8a9366e09493828e0a1d27f6d69a79fd + gRPC-Core: 642d29e59e5490374622b0629c2dd1c4c111775c + gRPC-ProtoRPC: 675ef3d484c06967ed2a5f5ee0e510a3756f755e + gRPC-RxLibrary: 7a25c5c25282669a82d1783d7e8a036f53e8ef27 GTMSessionFetcher: 5ad62e8200fa00ed011fe5e08d27fef72c5b1429 leveldb-library: 10fb39c39e243db4af1828441162405bbcec1404 nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 @@ -156,6 +194,6 @@ SPEC CHECKSUMS: RNFirebase: 60be8c01b94551a12e7be5431189e8ee8cefcdd3 Yoga: c90474ca3ec1edba44c97b6c381f03e222a9e287 -PODFILE CHECKSUM: 46b6a553f3c9fd264b449806b373d33b4af518b5 +PODFILE CHECKSUM: 49e66d8a1599e426396a3ba88a24baacc7f5423c COCOAPODS: 1.2.1 diff --git a/tests/src/main.firestore.js b/tests/src/main.firestore.js new file mode 100644 index 00000000..e3ce91fe --- /dev/null +++ b/tests/src/main.firestore.js @@ -0,0 +1,106 @@ +import React, { Component } from 'react'; +import { Text, View } from 'react-native'; +import fb from './firebase'; + +global.Promise = require('bluebird'); + +const firebase = fb.native; + + +function bootstrap() { + // Remove logging on production + if (!__DEV__) { + console.log = () => { + }; + console.warn = () => { + }; + console.error = () => { + }; + console.disableYellowBox = true; + } + + class Root extends Component { + + async componentDidMount() { + console.log(`Starting`); + const db = firebase.firestore(); + const docRef = await db.collection('chris').add({ first: 'Ada', last: 'Lovelace', born: 1815 }); + console.log(`Document written with ID: ${docRef.id}`); + const docRef2 = await db.collection('chris').add({ first: 'Alan', middle: 'Mathison', last: 'Turing', born: 1912 }); + console.log(`Document written with ID: ${docRef2.id}`); + await db.collection('chris').doc('manual').set({ first: 'Manual', last: 'Man', born: 1234 }); + console.log('Manual document set'); + await db.collection('chris').doc().set({ first: 'Auto', last: 'Man', born: 2000 }); + console.log('Auto document set'); + + const docRefT = db.doc(docRef.path); + const docRefS = await docRefT.get(); + console.log(`Should be the same as first written ID: ${docRefT.id}`, docRefS.data()); + + await docRefT.set({ empty: true }); + const docRefS2 = await docRefT.get(); + console.log(`Should have empty only: ${docRefT.id}`, docRefS2.data()); + + await docRefT.set({ first: 'Ada', last: 'Lovelace', born: 1815 }, { merge: true }); + const docRefS3 = await docRefT.get(); + console.log(`Should have everything plus empty: ${docRefT.id}`, docRefS3.data()); + + await docRefT.update({ first: 'AdaUpdated' }); + const docRefS4 = await docRefT.get(); + console.log(`Should have updated firstname: ${docRefT.id}`, docRefS4.data()); + + const docs = await db.collection('chris').get(); + const tasks = []; + docs.forEach((doc) => { + console.log(`Cleaning up ${doc.id}`, doc.data()); + tasks.push(doc.ref.delete()); + }); + Promise.all(tasks); + console.log('Finished cleaning collection'); + + const nycRef = db.collection('chris').doc('NYC'); + const sfRef = db.collection('chris').doc('SF'); + + await db.batch() + .set(nycRef, { name: 'New York City' }) + .set(sfRef, { name: 'San Francisco' }) + .commit(); + + const docs2 = await db.collection('chris').get(); + docs2.forEach((doc) => { + console.log(`Got ${doc.id}`, doc.data()); + }); + + await db.batch() + .update(nycRef, { population: 1000000 }) + .update(sfRef, { name: 'San Fran' }) + .commit(); + const docs3 = await db.collection('chris').get(); + docs3.forEach((doc) => { + console.log(`Got ${doc.id}`, doc.data()); + }); + + await db.batch() + .delete(nycRef) + .delete(sfRef) + .commit(); + const docs4 = await db.collection('chris').get(); + docs4.forEach((doc) => { + console.log(`Got ${doc.id}`, doc.data()); + }); + console.log('Finished'); + } + + render() { + return ( + + Check console logs + + ); + } + } + + return Root; +} + +export default bootstrap(); From 1d435a661408a920447e52ffa5a582d74e43c13c Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 28 Sep 2017 15:49:22 +0100 Subject: [PATCH 08/21] Bump flow-bin version to stop it hammering my Mac! --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 693ef25d..3010ee8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2624,9 +2624,9 @@ } }, "flow-bin": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.46.0.tgz", - "integrity": "sha1-Bq1/4Z3dsQQiZEOAZKKjL+4SuHI=", + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.55.0.tgz", + "integrity": "sha1-kIPakye9jKtrQHbWPYXyJHp+rhs=", "dev": true }, "for-in": { diff --git a/package.json b/package.json index bc9cbe0f..123305c0 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eslint-plugin-import": "^2.0.1", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.4.1", - "flow-bin": "^0.46.0", + "flow-bin": "^0.55.0", "react": "^15.3.0", "react-dom": "^15.3.0", "react-native": "^0.44.0", From b4743ffa8b8708659328c79427b65fbaae2d2e52 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 28 Sep 2017 17:48:13 +0100 Subject: [PATCH 09/21] [firestore][tests] Tests for most of the current functionality --- .../firestore/FirestoreSerialize.java | 4 +- .../RNFirebaseFirestoreDocumentReference.m | 4 +- tests/src/main.firestore.js | 106 --------------- tests/src/tests/firestore/collection/index.js | 30 ----- .../tests/firestore/collection/whereTests.js | 86 ------------ .../firestore/collectionReferenceTests.js | 123 ++++++++++++++++++ tests/src/tests/firestore/document/index.js | 22 ---- .../tests/firestore/documentReferenceTests.js | 80 ++++++++++++ tests/src/tests/firestore/firestoreTests.js | 63 +++++++++ tests/src/tests/firestore/index.js | 57 +++++++- 10 files changed, 324 insertions(+), 251 deletions(-) delete mode 100644 tests/src/main.firestore.js delete mode 100644 tests/src/tests/firestore/collection/index.js delete mode 100644 tests/src/tests/firestore/collection/whereTests.js create mode 100644 tests/src/tests/firestore/collectionReferenceTests.js delete mode 100644 tests/src/tests/firestore/document/index.js create mode 100644 tests/src/tests/firestore/documentReferenceTests.js create mode 100644 tests/src/tests/firestore/firestoreTests.js diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index b196a07d..d665ed6a 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -31,7 +31,9 @@ public class FirestoreSerialize { WritableMap documentMap = Arguments.createMap(); documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath()); - documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); + if (documentSnapshot.exists()) { + documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); + } // Missing fields from web SDK // createTime // readTime diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index 8b1b1bbb..e7516ee2 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -84,7 +84,9 @@ + (NSDictionary *)snapshotToDictionary:(FIRDocumentSnapshot *)documentSnapshot { NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; [snapshot setValue:documentSnapshot.reference.path forKey:@"path"]; - [snapshot setValue:documentSnapshot.data forKey:@"data"]; + if (documentSnapshot.exists) { + [snapshot setValue:documentSnapshot.data forKey:@"data"]; + } // Missing fields from web SDK // createTime // readTime diff --git a/tests/src/main.firestore.js b/tests/src/main.firestore.js deleted file mode 100644 index e3ce91fe..00000000 --- a/tests/src/main.firestore.js +++ /dev/null @@ -1,106 +0,0 @@ -import React, { Component } from 'react'; -import { Text, View } from 'react-native'; -import fb from './firebase'; - -global.Promise = require('bluebird'); - -const firebase = fb.native; - - -function bootstrap() { - // Remove logging on production - if (!__DEV__) { - console.log = () => { - }; - console.warn = () => { - }; - console.error = () => { - }; - console.disableYellowBox = true; - } - - class Root extends Component { - - async componentDidMount() { - console.log(`Starting`); - const db = firebase.firestore(); - const docRef = await db.collection('chris').add({ first: 'Ada', last: 'Lovelace', born: 1815 }); - console.log(`Document written with ID: ${docRef.id}`); - const docRef2 = await db.collection('chris').add({ first: 'Alan', middle: 'Mathison', last: 'Turing', born: 1912 }); - console.log(`Document written with ID: ${docRef2.id}`); - await db.collection('chris').doc('manual').set({ first: 'Manual', last: 'Man', born: 1234 }); - console.log('Manual document set'); - await db.collection('chris').doc().set({ first: 'Auto', last: 'Man', born: 2000 }); - console.log('Auto document set'); - - const docRefT = db.doc(docRef.path); - const docRefS = await docRefT.get(); - console.log(`Should be the same as first written ID: ${docRefT.id}`, docRefS.data()); - - await docRefT.set({ empty: true }); - const docRefS2 = await docRefT.get(); - console.log(`Should have empty only: ${docRefT.id}`, docRefS2.data()); - - await docRefT.set({ first: 'Ada', last: 'Lovelace', born: 1815 }, { merge: true }); - const docRefS3 = await docRefT.get(); - console.log(`Should have everything plus empty: ${docRefT.id}`, docRefS3.data()); - - await docRefT.update({ first: 'AdaUpdated' }); - const docRefS4 = await docRefT.get(); - console.log(`Should have updated firstname: ${docRefT.id}`, docRefS4.data()); - - const docs = await db.collection('chris').get(); - const tasks = []; - docs.forEach((doc) => { - console.log(`Cleaning up ${doc.id}`, doc.data()); - tasks.push(doc.ref.delete()); - }); - Promise.all(tasks); - console.log('Finished cleaning collection'); - - const nycRef = db.collection('chris').doc('NYC'); - const sfRef = db.collection('chris').doc('SF'); - - await db.batch() - .set(nycRef, { name: 'New York City' }) - .set(sfRef, { name: 'San Francisco' }) - .commit(); - - const docs2 = await db.collection('chris').get(); - docs2.forEach((doc) => { - console.log(`Got ${doc.id}`, doc.data()); - }); - - await db.batch() - .update(nycRef, { population: 1000000 }) - .update(sfRef, { name: 'San Fran' }) - .commit(); - const docs3 = await db.collection('chris').get(); - docs3.forEach((doc) => { - console.log(`Got ${doc.id}`, doc.data()); - }); - - await db.batch() - .delete(nycRef) - .delete(sfRef) - .commit(); - const docs4 = await db.collection('chris').get(); - docs4.forEach((doc) => { - console.log(`Got ${doc.id}`, doc.data()); - }); - console.log('Finished'); - } - - render() { - return ( - - Check console logs - - ); - } - } - - return Root; -} - -export default bootstrap(); diff --git a/tests/src/tests/firestore/collection/index.js b/tests/src/tests/firestore/collection/index.js deleted file mode 100644 index d46d4e9e..00000000 --- a/tests/src/tests/firestore/collection/index.js +++ /dev/null @@ -1,30 +0,0 @@ -// import docSnapTests from './docSnapTests'; -// import querySnapTests from './querySnapTests'; -// import onSnapshotTests from './onSnapshotTests'; -// import bugTests from './bugTests'; -import whereTests from './whereTests'; - -const testGroups = [ - // onSnapshotTests, - // querySnapTests, - // docSnapTests, - // bugTests, - whereTests, -]; - -function registerTestSuite(testSuite) { - testSuite.beforeEach(async function () { - // todo reset test data - }); - - testSuite.afterEach(async function () { - // todo reset test data - }); - - testGroups.forEach((testGroup) => { - testGroup(testSuite); - }); -} - - -module.exports = registerTestSuite; diff --git a/tests/src/tests/firestore/collection/whereTests.js b/tests/src/tests/firestore/collection/whereTests.js deleted file mode 100644 index 9dfe6da7..00000000 --- a/tests/src/tests/firestore/collection/whereTests.js +++ /dev/null @@ -1,86 +0,0 @@ -import should from 'should'; - -/** - -Test document structure from fb console: - - baz: true - daz: 123 - foo: "bar" - gaz: 12.1234567 - naz: null - -*/ - -function whereTests({ describe, it, context, firebase }) { - describe('CollectionReference.where()', () => { - context('correctly handles', () => { - it('== boolean values', () => { - return firebase.native.firestore() - .collection('tests') - .where('baz', '==', true) - .get() - .then((querySnapshot) => { - should.equal(querySnapshot.size, 1); - querySnapshot.forEach((documentSnapshot) => { - should.equal(documentSnapshot.data().baz, true); - }); - }); - }); - - it('== string values', () => { - return firebase.native.firestore() - .collection('tests') - .where('foo', '==', 'bar') - .get() - .then((querySnapshot) => { - should.equal(querySnapshot.size, 1); - querySnapshot.forEach((documentSnapshot) => { - should.equal(documentSnapshot.data().foo, 'bar'); - }); - }); - }); - - it('== null values', () => { - return firebase.native.firestore() - .collection('tests') - .where('naz', '==', null) - .get() - .then((querySnapshot) => { - should.equal(querySnapshot.size, 1); - querySnapshot.forEach((documentSnapshot) => { - should.equal(documentSnapshot.data().naz, null); - }); - }); - }); - - it('>= number values', () => { - return firebase.native.firestore() - .collection('tests') - .where('daz', '>=', 123) - .get() - .then((querySnapshot) => { - should.equal(querySnapshot.size, 1); - querySnapshot.forEach((documentSnapshot) => { - should.equal(documentSnapshot.data().daz, 123); - }); - }); - }); - - it('<= float values', () => { - return firebase.native.firestore() - .collection('tests') - .where('gaz', '<=', 12.1234666) - .get() - .then((querySnapshot) => { - should.equal(querySnapshot.size, 1); - querySnapshot.forEach((documentSnapshot) => { - should.equal(documentSnapshot.data().gaz, 12.1234567); - }); - }); - }); - }); - }); -} - -export default whereTests; diff --git a/tests/src/tests/firestore/collectionReferenceTests.js b/tests/src/tests/firestore/collectionReferenceTests.js new file mode 100644 index 00000000..d0ba525d --- /dev/null +++ b/tests/src/tests/firestore/collectionReferenceTests.js @@ -0,0 +1,123 @@ +import should from 'should'; + +function collectionReferenceTests({ describe, it, context, firebase }) { + describe('CollectionReference', () => { + context('class', () => { + it('should return instance methods', () => { + return new Promise((resolve) => { + const collection = firebase.native.firestore().collection('collection-tests'); + collection.should.have.property('firestore'); + // TODO: Remaining checks + + resolve(); + }); + }); + }); + + context('add()', () => { + it('should create Document', () => { + return firebase.native.firestore() + .collection('collection-tests') + .add({ first: 'Ada', last: 'Lovelace', born: 1815 }) + .then(async (docRef) => { + const doc = await firebase.native.firestore().doc(docRef.path).get(); + doc.data().first.should.equal('Ada'); + }); + }); + }); + + context('doc()', () => { + it('should create DocumentReference with correct path', () => { + return new Promise((resolve) => { + const docRef = firebase.native.firestore().collection('collection-tests').doc('doc'); + should.equal(docRef.path, 'collection-tests/doc'); + resolve(); + }); + }); + }); + + context('get()', () => { + it('should retrieve a single document', () => { + return firebase.native.firestore() + .collection('collection-tests') + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().baz, true); + }); + }); + }); + }); + + // Where + context('where()', () => { + it('correctly handles == boolean values', () => { + return firebase.native.firestore() + .collection('collection-tests') + .where('baz', '==', true) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().baz, true); + }); + }); + }); + + it('correctly handles == string values', () => { + return firebase.native.firestore() + .collection('collection-tests') + .where('foo', '==', 'bar') + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().foo, 'bar'); + }); + }); + }); + + it('correctly handles == null values', () => { + return firebase.native.firestore() + .collection('collection-tests') + .where('naz', '==', null) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().naz, null); + }); + }); + }); + + it('correctly handles >= number values', () => { + return firebase.native.firestore() + .collection('collection-tests') + .where('daz', '>=', 123) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().daz, 123); + }); + }); + }); + + it('correctly handles <= float values', () => { + return firebase.native.firestore() + .collection('collection-tests') + .where('gaz', '<=', 12.1234666) + .get() + .then((querySnapshot) => { + should.equal(querySnapshot.size, 1); + querySnapshot.forEach((documentSnapshot) => { + should.equal(documentSnapshot.data().gaz, 12.1234567); + }); + }); + }); + }); + }); +} + +export default collectionReferenceTests; diff --git a/tests/src/tests/firestore/document/index.js b/tests/src/tests/firestore/document/index.js deleted file mode 100644 index d6949789..00000000 --- a/tests/src/tests/firestore/document/index.js +++ /dev/null @@ -1,22 +0,0 @@ -// import whereTests from './whereTests'; - -const testGroups = [ - // whereTests, -]; - -function registerTestSuite(testSuite) { - testSuite.beforeEach(async function () { - // todo reset test data - }); - - testSuite.afterEach(async function () { - // todo reset test data - }); - - testGroups.forEach((testGroup) => { - testGroup(testSuite); - }); -} - - -module.exports = registerTestSuite; diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js new file mode 100644 index 00000000..5242583b --- /dev/null +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -0,0 +1,80 @@ +import should from 'should'; + +function collectionReferenceTests({ describe, it, context, firebase }) { + describe('DocumentReference', () => { + context('class', () => { + it('should return instance methods', () => { + return new Promise((resolve) => { + const document = firebase.native.firestore().doc('document-tests/doc1'); + document.should.have.property('firestore'); + // TODO: Remaining checks + + resolve(); + }); + }); + }); + + context('delete()', () => { + it('should delete Document', () => { + return firebase.native.firestore() + .doc('document-tests/doc1') + .delete() + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc1').get(); + should.equal(doc.exists, false); + }); + }); + }); + + context('set()', () => { + it('should create Document', () => { + return firebase.native.firestore() + .doc('document-tests/doc2') + .set({ name: 'doc2' }) + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc2').get(); + doc.data().name.should.equal('doc2'); + }); + }); + }); + + context('set()', () => { + it('should merge Document', () => { + return firebase.native.firestore() + .doc('document-tests/doc1') + .set({ merge: 'merge' }, { merge: true }) + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc1').get(); + doc.data().name.should.equal('doc1'); + doc.data().merge.should.equal('merge'); + }); + }); + }); + + context('set()', () => { + it('should overwrite Document', () => { + return firebase.native.firestore() + .doc('document-tests/doc1') + .set({ name: 'overwritten' }) + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc1').get(); + doc.data().name.should.equal('overwritten'); + }); + }); + }); + + context('update()', () => { + it('should update Document', () => { + return firebase.native.firestore() + .doc('document-tests/doc1') + .set({ name: 'updated' }) + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc1').get(); + doc.data().name.should.equal('updated'); + }); + }); + }); + }); +} + +export default collectionReferenceTests; diff --git a/tests/src/tests/firestore/firestoreTests.js b/tests/src/tests/firestore/firestoreTests.js new file mode 100644 index 00000000..26a29893 --- /dev/null +++ b/tests/src/tests/firestore/firestoreTests.js @@ -0,0 +1,63 @@ +import should from 'should'; + +function firestoreTests({ describe, it, context, firebase }) { + describe('firestore()', () => { + context('collection()', () => { + it('should create CollectionReference with the right id', () => { + return new Promise((resolve) => { + const collectionRef = firebase.native.firestore().collection('collection1/doc1/collection2'); + should.equal(collectionRef.id, 'collection2'); + resolve(); + }); + }); + }); + + context('doc()', () => { + it('should create DocumentReference with correct path', () => { + return new Promise((resolve) => { + const docRef = firebase.native.firestore().doc('collection1/doc1/collection2/doc2'); + should.equal(docRef.path, 'collection1/doc1/collection2/doc2'); + resolve(); + }); + }); + }); + + context('batch()', () => { + it('should create / update / delete as expected', () => { + const ayRef = firebase.native.firestore().collection('firestore-tests').doc('AY'); + const lRef = firebase.native.firestore().collection('firestore-tests').doc('LON'); + const nycRef = firebase.native.firestore().collection('firestore-tests').doc('NYC'); + const sfRef = firebase.native.firestore().collection('firestore-tests').doc('SF'); + + return firebase.native.firestore() + .batch() + .set(ayRef, { name: 'Aylesbury' }) + .set(lRef, { name: 'London' }) + .set(nycRef, { name: 'New York City' }) + .set(sfRef, { name: 'San Francisco' }) + .update(nycRef, { population: 1000000 }) + .update(sfRef, { name: 'San Fran' }) + .set(lRef, { population: 3000000 }, { merge: true }) + .delete(ayRef) + .commit() + .then(async () => { + const ayDoc = await ayRef.get(); + should.equal(ayDoc.exists, false); + + const lDoc = await lRef.get(); + lDoc.data().name.should.equal('London'); + lDoc.data().population.should.equal(3000000); + + const nycDoc = await nycRef.get(); + nycDoc.data().name.should.equal('New York City'); + nycDoc.data().population.should.equal(1000000); + + const sfDoc = await sfRef.get(); + sfDoc.data().name.should.equal('San Fran'); + }); + }); + }); + }); +} + +export default firestoreTests; diff --git a/tests/src/tests/firestore/index.js b/tests/src/tests/firestore/index.js index 20990491..22e4b734 100644 --- a/tests/src/tests/firestore/index.js +++ b/tests/src/tests/firestore/index.js @@ -4,16 +4,63 @@ import TestSuite from '../../../lib/TestSuite'; /* Test suite files */ -import collectionTestGroups from './collection/index'; -import documentTestGroups from './document/index'; +import collectionReferenceTests from './collectionReferenceTests'; +import documentReferenceTests from './documentReferenceTests'; +import firestoreTests from './firestoreTests'; const suite = new TestSuite('Firestore', 'firebase.firestore()', firebase); +const testGroups = [ + collectionReferenceTests, + documentReferenceTests, + firestoreTests, +]; + +function firestoreTestSuite(testSuite) { + testSuite.beforeEach(async () => { + this.collectionTestsCollection = testSuite.firebase.native.firestore().collection('collection-tests'); + this.documentTestsCollection = testSuite.firebase.native.firestore().collection('document-tests'); + this.firestoreTestsCollection = testSuite.firebase.native.firestore().collection('firestore-tests'); + // Clean the collections in case the last run failed + await cleanCollection(this.collectionTestsCollection); + await cleanCollection(this.documentTestsCollection); + await cleanCollection(this.firestoreTestsCollection); + + await this.collectionTestsCollection.add({ + baz: true, + daz: 123, + foo: 'bar', + gaz: 12.1234567, + naz: null, + }); + + await this.documentTestsCollection.doc('doc1').set({ + name: 'doc1', + }); + }); + + testSuite.afterEach(async () => { + await cleanCollection(this.collectionTestsCollection); + await cleanCollection(this.documentTestsCollection); + await cleanCollection(this.firestoreTestsCollection); + }); + + testGroups.forEach((testGroup) => { + testGroup(testSuite); + }); +} + /* Register tests with test suite */ - -suite.addTests(documentTestGroups); -suite.addTests(collectionTestGroups); +suite.addTests(firestoreTestSuite); export default suite; + +/* HELPER FUNCTIONS */ +async function cleanCollection(collection) { + const collectionTestsDocs = await collection.get(); + const tasks = []; + collectionTestsDocs.forEach(doc => tasks.push(doc.ref.delete())); + await Promise.all(tasks); +} From cda1c27b5c2836ba6750b58b3898a91f9da7d083 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 2 Oct 2017 13:11:38 +0100 Subject: [PATCH 10/21] [firestore][android][js] Add document `onSnapshot` support plus tests --- .../io/invertase/firebase/ErrorUtils.java | 27 +++ .../firebase/database/RNFirebaseDatabase.java | 77 +++---- .../database/RNFirebaseDatabaseReference.java | 6 +- .../firestore/RNFirebaseFirestore.java | 156 ++++++++++++- ...NFirebaseFirestoreCollectionReference.java | 3 +- .../RNFirebaseFirestoreDocumentReference.java | 89 ++++++- lib/modules/firestore/DocumentReference.js | 44 +++- lib/modules/firestore/Query.js | 2 +- lib/modules/firestore/index.js | 53 +++++ lib/utils/ModuleBase.js | 4 + .../tests/firestore/documentReferenceTests.js | 217 ++++++++++++++++++ 11 files changed, 597 insertions(+), 81 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/ErrorUtils.java diff --git a/android/src/main/java/io/invertase/firebase/ErrorUtils.java b/android/src/main/java/io/invertase/firebase/ErrorUtils.java new file mode 100644 index 00000000..85d4fdb0 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/ErrorUtils.java @@ -0,0 +1,27 @@ +package io.invertase.firebase; + +public class ErrorUtils { + /** + * Wrap a message string with the specified service name e.g. 'Database' + * + * @param message + * @param service + * @param fullCode + * @return + */ + public static String getMessageWithService(String message, String service, String fullCode) { + // Service: Error message (service/code). + return service + ": " + message + " (" + fullCode.toLowerCase() + ")."; + } + + /** + * Generate a service error code string e.g. 'DATABASE/PERMISSION-DENIED' + * + * @param service + * @param code + * @return + */ + public static String getCodeWithService(String service, String code) { + return service.toLowerCase() + "/" + code.toLowerCase(); + } +} diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java index 07a91893..6612f491 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabase.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import io.invertase.firebase.ErrorUtils; import io.invertase.firebase.Utils; @@ -522,30 +523,6 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { return existingRef; } - /** - * Wrap a message string with the specified service name e.g. 'Database' - * - * @param message - * @param service - * @param fullCode - * @return - */ - private static String getMessageWithService(String message, String service, String fullCode) { - // Service: Error message (service/code). - return service + ": " + message + " (" + fullCode.toLowerCase() + ")."; - } - - /** - * Generate a service error code string e.g. 'DATABASE/PERMISSION-DENIED' - * - * @param service - * @param code - * @return - */ - private static String getCodeWithService(String service, String code) { - return service.toLowerCase() + "/" + code.toLowerCase(); - } - /** * Convert as firebase DatabaseError instance into a writable map * with the correct web-like error codes. @@ -564,56 +541,56 @@ public class RNFirebaseDatabase extends ReactContextBaseJavaModule { switch (nativeError.getCode()) { case DatabaseError.DATA_STALE: - code = getCodeWithService(service, "data-stale"); - message = getMessageWithService("The transaction needs to be run again with current data.", service, code); + code = ErrorUtils.getCodeWithService(service, "data-stale"); + message = ErrorUtils.getMessageWithService("The transaction needs to be run again with current data.", service, code); break; case DatabaseError.OPERATION_FAILED: - code = getCodeWithService(service, "failure"); - message = getMessageWithService("The server indicated that this operation failed.", service, code); + code = ErrorUtils.getCodeWithService(service, "failure"); + message = ErrorUtils.getMessageWithService("The server indicated that this operation failed.", service, code); break; case DatabaseError.PERMISSION_DENIED: - code = getCodeWithService(service, "permission-denied"); - message = getMessageWithService("Client doesn't have permission to access the desired data.", service, code); + code = ErrorUtils.getCodeWithService(service, "permission-denied"); + message = ErrorUtils.getMessageWithService("Client doesn't have permission to access the desired data.", service, code); break; case DatabaseError.DISCONNECTED: - code = getCodeWithService(service, "disconnected"); - message = getMessageWithService("The operation had to be aborted due to a network disconnect.", service, code); + code = ErrorUtils.getCodeWithService(service, "disconnected"); + message = ErrorUtils.getMessageWithService("The operation had to be aborted due to a network disconnect.", service, code); break; case DatabaseError.EXPIRED_TOKEN: - code = getCodeWithService(service, "expired-token"); - message = getMessageWithService("The supplied auth token has expired.", service, code); + code = ErrorUtils.getCodeWithService(service, "expired-token"); + message = ErrorUtils.getMessageWithService("The supplied auth token has expired.", service, code); break; case DatabaseError.INVALID_TOKEN: - code = getCodeWithService(service, "invalid-token"); - message = getMessageWithService("The supplied auth token was invalid.", service, code); + code = ErrorUtils.getCodeWithService(service, "invalid-token"); + message = ErrorUtils.getMessageWithService("The supplied auth token was invalid.", service, code); break; case DatabaseError.MAX_RETRIES: - code = getCodeWithService(service, "max-retries"); - message = getMessageWithService("The transaction had too many retries.", service, code); + code = ErrorUtils.getCodeWithService(service, "max-retries"); + message = ErrorUtils.getMessageWithService("The transaction had too many retries.", service, code); break; case DatabaseError.OVERRIDDEN_BY_SET: - code = getCodeWithService(service, "overridden-by-set"); - message = getMessageWithService("The transaction was overridden by a subsequent set.", service, code); + code = ErrorUtils.getCodeWithService(service, "overridden-by-set"); + message = ErrorUtils.getMessageWithService("The transaction was overridden by a subsequent set.", service, code); break; case DatabaseError.UNAVAILABLE: - code = getCodeWithService(service, "unavailable"); - message = getMessageWithService("The service is unavailable.", service, code); + code = ErrorUtils.getCodeWithService(service, "unavailable"); + message = ErrorUtils.getMessageWithService("The service is unavailable.", service, code); break; case DatabaseError.USER_CODE_EXCEPTION: - code = getCodeWithService(service, "user-code-exception"); - message = getMessageWithService("User code called from the Firebase Database runloop threw an exception.", service, code); + code = ErrorUtils.getCodeWithService(service, "user-code-exception"); + message = ErrorUtils.getMessageWithService("User code called from the Firebase Database runloop threw an exception.", service, code); break; case DatabaseError.NETWORK_ERROR: - code = getCodeWithService(service, "network-error"); - message = getMessageWithService("The operation could not be performed due to a network error.", service, code); + code = ErrorUtils.getCodeWithService(service, "network-error"); + message = ErrorUtils.getMessageWithService("The operation could not be performed due to a network error.", service, code); break; case DatabaseError.WRITE_CANCELED: - code = getCodeWithService(service, "write-cancelled"); - message = getMessageWithService("The write was canceled by the user.", service, code); + code = ErrorUtils.getCodeWithService(service, "write-cancelled"); + message = ErrorUtils.getMessageWithService("The write was canceled by the user.", service, code); break; default: - code = getCodeWithService(service, "unknown"); - message = getMessageWithService("An unknown error occurred.", service, code); + code = ErrorUtils.getCodeWithService(service, "unknown"); + message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code); } errorMap.putString("code", code); diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java index e6c2216b..60a6cbe5 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java @@ -29,8 +29,8 @@ class RNFirebaseDatabaseReference { private String appName; private ReactContext reactContext; private static final String TAG = "RNFirebaseDBReference"; - private HashMap childEventListeners; - private HashMap valueEventListeners; + private HashMap childEventListeners = new HashMap<>(); + private HashMap valueEventListeners = new HashMap<>(); /** * RNFirebase wrapper around FirebaseDatabaseReference, @@ -47,8 +47,6 @@ class RNFirebaseDatabaseReference { query = null; appName = app; reactContext = context; - childEventListeners = new HashMap<>(); - valueEventListeners = new HashMap<>(); buildDatabaseQueryAtPathAndModifiers(refPath, modifiersArray); } diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index 968f5403..19c0f21b 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -11,12 +11,14 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApp; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FieldValue; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.SetOptions; import com.google.firebase.firestore.WriteBatch; @@ -24,12 +26,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import io.invertase.firebase.ErrorUtils; import io.invertase.firebase.Utils; public class RNFirebaseFirestore extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseFirestore"; - // private HashMap references = new HashMap<>(); + private HashMap collectionReferences = new HashMap<>(); + private HashMap documentReferences = new HashMap<>(); // private SparseArray transactionHandlers = new SparseArray<>(); RNFirebaseFirestore(ReactApplicationContext reactContext) { @@ -94,7 +98,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { promise.resolve(result); } else { Log.e(TAG, "set:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); } } }); @@ -129,6 +133,22 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { // Not supported on Android out of the box } + @ReactMethod + public void documentOffSnapshot(String appName, String path, int listenerId) { + RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path); + ref.offSnapshot(listenerId); + + if (!ref.hasListeners()) { + clearCachedDocumentForAppPath(appName, path); + } + } + + @ReactMethod + public void documentOnSnapshot(String appName, String path, int listenerId) { + RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path); + ref.onSnapshot(listenerId); + } + @ReactMethod public void documentSet(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) { RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); @@ -151,12 +171,11 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { * @param exception Exception Exception normally from a task result. * @param promise Promise react native promise */ - static void promiseRejectException(Promise promise, Exception exception) { - // TODO - // WritableMap jsError = getJSError(exception); + static void promiseRejectException(Promise promise, FirebaseFirestoreException exception) { + WritableMap jsError = getJSError(exception); promise.reject( - "TODO", // jsError.getString("code"), - exception.getMessage(), // jsError.getString("message"), + jsError.getString("code"), + jsError.getString("message"), exception ); } @@ -188,6 +207,35 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { return new RNFirebaseFirestoreCollectionReference(appName, path, filters, orders, options); } + /** + * Get a cached document reference for a specific app and path + * + * @param appName + * @param path + * @return + */ + private RNFirebaseFirestoreDocumentReference getCachedDocumentForAppPath(String appName, String path) { + String key = appName + "/" + path; + RNFirebaseFirestoreDocumentReference ref = documentReferences.get(key); + if (ref == null) { + ref = getDocumentForAppPath(appName, path); + documentReferences.put(key, ref); + } + return ref; + } + + /** + * Clear a cached document reference for a specific app and path + * + * @param appName + * @param path + * @return + */ + private void clearCachedDocumentForAppPath(String appName, String path) { + String key = appName + "/" + path; + documentReferences.remove(key); + } + /** * Get a document reference for a specific app and path * @@ -196,7 +244,99 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { * @return */ private RNFirebaseFirestoreDocumentReference getDocumentForAppPath(String appName, String path) { - return new RNFirebaseFirestoreDocumentReference(appName, path); + return new RNFirebaseFirestoreDocumentReference(this.getReactApplicationContext(), appName, path); + } + + /** + * Convert as firebase DatabaseError instance into a writable map + * with the correct web-like error codes. + * + * @param nativeException + * @return + */ + static WritableMap getJSError(FirebaseFirestoreException nativeException) { + WritableMap errorMap = Arguments.createMap(); + errorMap.putInt("nativeErrorCode", nativeException.getCode().value()); + errorMap.putString("nativeErrorMessage", nativeException.getMessage()); + + String code; + String message; + String service = "Firestore"; + + // TODO: Proper error mappings + switch (nativeException.getCode()) { + case OK: + code = ErrorUtils.getCodeWithService(service, "ok"); + message = ErrorUtils.getMessageWithService("Ok.", service, code); + break; + case CANCELLED: + code = ErrorUtils.getCodeWithService(service, "cancelled"); + message = ErrorUtils.getMessageWithService("Cancelled.", service, code); + break; + case UNKNOWN: + code = ErrorUtils.getCodeWithService(service, "unknown"); + message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code); + break; + case INVALID_ARGUMENT: + code = ErrorUtils.getCodeWithService(service, "invalid-argument"); + message = ErrorUtils.getMessageWithService("Invalid argument.", service, code); + break; + case NOT_FOUND: + code = ErrorUtils.getCodeWithService(service, "not-found"); + message = ErrorUtils.getMessageWithService("Not found.", service, code); + break; + case ALREADY_EXISTS: + code = ErrorUtils.getCodeWithService(service, "already-exists"); + message = ErrorUtils.getMessageWithService("Already exists.", service, code); + break; + case PERMISSION_DENIED: + code = ErrorUtils.getCodeWithService(service, "permission-denied"); + message = ErrorUtils.getMessageWithService("Permission denied.", service, code); + break; + case RESOURCE_EXHAUSTED: + code = ErrorUtils.getCodeWithService(service, "resource-exhausted"); + message = ErrorUtils.getMessageWithService("Resource exhausted.", service, code); + break; + case FAILED_PRECONDITION: + code = ErrorUtils.getCodeWithService(service, "failed-precondition"); + message = ErrorUtils.getMessageWithService("Failed precondition.", service, code); + break; + case ABORTED: + code = ErrorUtils.getCodeWithService(service, "aborted"); + message = ErrorUtils.getMessageWithService("Aborted.", service, code); + break; + case OUT_OF_RANGE: + code = ErrorUtils.getCodeWithService(service, "out-of-range"); + message = ErrorUtils.getMessageWithService("Out of range.", service, code); + break; + case UNIMPLEMENTED: + code = ErrorUtils.getCodeWithService(service, "unimplemented"); + message = ErrorUtils.getMessageWithService("Unimplemented.", service, code); + break; + case INTERNAL: + code = ErrorUtils.getCodeWithService(service, "internal"); + message = ErrorUtils.getMessageWithService("Internal.", service, code); + break; + case UNAVAILABLE: + code = ErrorUtils.getCodeWithService(service, "unavailable"); + message = ErrorUtils.getMessageWithService("Unavailable.", service, code); + break; + case DATA_LOSS: + code = ErrorUtils.getCodeWithService(service, "data-loss"); + message = ErrorUtils.getMessageWithService("Data loss.", service, code); + break; + case UNAUTHENTICATED: + code = ErrorUtils.getCodeWithService(service, "unauthenticated"); + message = ErrorUtils.getMessageWithService("Unauthenticated.", service, code); + break; + default: + code = ErrorUtils.getCodeWithService(service, "unknown"); + message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code); + } + + errorMap.putString("code", code); + errorMap.putString("message", message); + return errorMap; } /** diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java index 9f41ab6a..9cba86be 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java @@ -10,6 +10,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; @@ -47,7 +48,7 @@ public class RNFirebaseFirestoreCollectionReference { promise.resolve(data); } else { Log.e(TAG, "get:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); } } }); diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java index 6d5bfed1..ffe442a0 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java @@ -5,14 +5,19 @@ import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.EventListener; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.SetOptions; +import java.util.HashMap; import java.util.Map; import io.invertase.firebase.Utils; @@ -22,11 +27,14 @@ public class RNFirebaseFirestoreDocumentReference { private static final String TAG = "RNFBFSDocumentReference"; private final String appName; private final String path; + private ReactContext reactContext; private final DocumentReference ref; + private Map documentSnapshotListeners = new HashMap<>(); - RNFirebaseFirestoreDocumentReference(String appName, String path) { + RNFirebaseFirestoreDocumentReference(ReactContext reactContext, String appName, String path) { this.appName = appName; this.path = path; + this.reactContext = reactContext; this.ref = RNFirebaseFirestore.getFirestoreForApp(appName).document(path); } @@ -49,7 +57,7 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(Arguments.createMap()); } else { Log.e(TAG, "delete:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); } } }); @@ -65,12 +73,40 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(data); } else { Log.e(TAG, "get:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); } } }); } + public void offSnapshot(final int listenerId) { + ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + } + } + + public void onSnapshot(final int listenerId) { + if (!documentSnapshotListeners.containsKey(listenerId)) { + final EventListener listener = new EventListener() { + @Override + public void onEvent(DocumentSnapshot documentSnapshot, FirebaseFirestoreException exception) { + if (exception == null) { + handleDocumentSnapshotEvent(listenerId, documentSnapshot); + } else { + ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + } + handleDocumentSnapshotError(listenerId, exception); + } + } + }; + ListenerRegistration listenerRegistration = this.ref.addSnapshotListener(listener); + documentSnapshotListeners.put(listenerId, listenerRegistration); + } + } + public void set(final ReadableMap data, final ReadableMap options, final Promise promise) { Map map = Utils.recursivelyDeconstructReadableMap(data); Task task; @@ -90,7 +126,7 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(Arguments.createMap()); } else { Log.e(TAG, "set:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); } } }); @@ -108,9 +144,52 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(Arguments.createMap()); } else { Log.e(TAG, "update:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, task.getException()); + RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); } } }); } + + /* + * INTERNALS/UTILS + */ + + public boolean hasListeners() { + return !documentSnapshotListeners.isEmpty(); + } + + /** + * Handles documentSnapshot events. + * + * @param listenerId + * @param documentSnapshot + */ + private void handleDocumentSnapshotEvent(int listenerId, DocumentSnapshot documentSnapshot) { + WritableMap event = Arguments.createMap(); + WritableMap data = FirestoreSerialize.snapshotToWritableMap(documentSnapshot); + + event.putString("appName", appName); + event.putString("path", path); + event.putInt("listenerId", listenerId); + event.putMap("document", data); + + Utils.sendEvent(reactContext, "firestore_document_sync_event", event); + } + + /** + * Handles a documentSnapshot error event + * + * @param listenerId + * @param exception + */ + private void handleDocumentSnapshotError(int listenerId, FirebaseFirestoreException exception) { + WritableMap event = Arguments.createMap(); + + event.putString("appName", appName); + event.putString("path", path); + event.putInt("listenerId", listenerId); + event.putMap("error", RNFirebaseFirestore.getJSError(exception)); + + Utils.sendEvent(reactContext, "firestore_document_sync_event", event); + } } diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js index f569f3d1..a061ca18 100644 --- a/lib/modules/firestore/DocumentReference.js +++ b/lib/modules/firestore/DocumentReference.js @@ -19,6 +19,9 @@ export type WriteResult = { writeTime: string, } +// track all event registrations +let listeners = 0; + /** * @class DocumentReference */ @@ -89,8 +92,30 @@ export default class DocumentReference { throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('DocumentReference', 'getCollections')); } - onSnapshot(onNext: () => any, onError?: () => any): () => void { - // TODO + onSnapshot(onNext: Function, onError?: Function): () => void { + // TODO: Validation + const listenerId = listeners++; + + // Listen to snapshot events + this._firestore.on( + this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), + onNext, + ); + + // Listen for snapshot error events + if (onError) { + this._firestore.on( + this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`), + onError, + ); + } + + // Add the native listener + this._firestore._native + .documentOnSnapshot(this.path, listenerId); + + // Return an unsubscribe method + return this._offDocumentSnapshot.bind(this, listenerId, onNext); } set(data: { [string]: any }, writeOptions?: WriteOptions): Promise { @@ -105,16 +130,11 @@ export default class DocumentReference { } /** - * INTERNALS + * Remove auth change listener + * @param listener */ - - /** - * Generate a string that uniquely identifies this DocumentReference - * - * @return {string} - * @private - */ - _getDocumentKey() { - return `$${this._firestore._appName}$/${this.path}`; + _offDocumentSnapshot(listenerId: number, listener: Function) { + this._firestore.log.info('Removing onDocumentSnapshot listener'); + this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), listener); } } diff --git a/lib/modules/firestore/Query.js b/lib/modules/firestore/Query.js index 4ec64817..e415ff0b 100644 --- a/lib/modules/firestore/Query.js +++ b/lib/modules/firestore/Query.js @@ -200,7 +200,7 @@ export default class Query { } stream(): Stream { - + throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('Query', 'stream')); } where(fieldPath: string, opStr: Operator, value: any): Query { diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js index b5849ece..e50f0482 100644 --- a/lib/modules/firestore/index.js +++ b/lib/modules/firestore/index.js @@ -16,6 +16,14 @@ import INTERNALS from './../../internals'; const unquotedIdentifier_ = '(?:[A-Za-z_][A-Za-z_0-9]*)'; const UNQUOTED_IDENTIFIER_REGEX = new RegExp(`^${unquotedIdentifier_}$`); +type DocumentSyncEvent = { + appName: string, + document?: DocumentSnapshot, + error?: Object, + listenerId: number, + path: string, +} + /** * @class Firestore */ @@ -28,6 +36,20 @@ export default class Firestore extends ModuleBase { constructor(firebaseApp: Object, options: Object = {}) { super(firebaseApp, options, true); this._referencePath = new Path([]); + + this.addListener( + // sub to internal native event - this fans out to + // public event name: onCollectionSnapshot + this._getAppEventName('firestore_collection_sync_event'), + this._onCollectionSyncEvent.bind(this), + ); + + this.addListener( + // sub to internal native event - this fans out to + // public event name: onDocumentSnapshot + this._getAppEventName('firestore_document_sync_event'), + this._onDocumentSyncEvent.bind(this), + ); } batch(): WriteBatch { @@ -107,6 +129,37 @@ export default class Firestore extends ModuleBase { return fieldPath; } + + /** + * INTERNALS + */ + + /** + * Internal collection sync listener + * @param event + * @private + */ + _onCollectionSyncEvent(event: DocumentSyncEvent) { + if (event.error) { + this.emit(this._getAppEventName(`onCollectionSnapshotError:${event.listenerId}`, event.error)); + } else { + this.emit(this._getAppEventName(`onCollectionSnapshot:${event.listenerId}`, event.document)); + } + } + + /** + * Internal document sync listener + * @param event + * @private + */ + _onDocumentSyncEvent(event: DocumentSyncEvent) { + if (event.error) { + this.emit(this._getAppEventName(`onDocumentSnapshotError:${event.listenerId}`), event.error); + } else { + const snapshot = new DocumentSnapshot(this, event.document); + this.emit(this._getAppEventName(`onDocumentSnapshot:${event.listenerId}`), snapshot); + } + } } export const statics = { diff --git a/lib/utils/ModuleBase.js b/lib/utils/ModuleBase.js index 127050f9..e8c63ef4 100644 --- a/lib/utils/ModuleBase.js +++ b/lib/utils/ModuleBase.js @@ -29,6 +29,10 @@ const NATIVE_MODULE_EVENTS = { 'database_transaction_event', // 'database_server_offset', // TODO ], + Firestore: [ + 'firestore_collection_sync_event', + 'firestore_document_sync_event', + ], }; const DEFAULTS = { diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js index 5242583b..f8b9afd6 100644 --- a/tests/src/tests/firestore/documentReferenceTests.js +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -1,3 +1,5 @@ +import sinon from 'sinon'; +import 'should-sinon'; import should from 'should'; function collectionReferenceTests({ describe, it, context, firebase }) { @@ -26,6 +28,221 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); + context('onSnapshot()', () => { + it('calls callback with the initial data and then when value changes', () => { + return new Promise(async (resolve) => { + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const currentDataValue = { name: 'doc1' }; + const newDataValue = { name: 'updated' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise((resolve2) => { + unsubscribe = docRef.onSnapshot((snapshot) => { + callback(snapshot.data()); + resolve2(); + }); + }); + + callback.should.be.calledWith(currentDataValue); + + await docRef.set(newDataValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + // Assertions + + callback.should.be.calledWith(newDataValue); + callback.should.be.calledTwice(); + + // Tear down + + unsubscribe(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('doesn\'t call callback when the ref is updated with the same value', async () => { + return new Promise(async (resolve) => { + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const currentDataValue = { name: 'doc1' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise((resolve2) => { + unsubscribe = docRef.onSnapshot((snapshot) => { + callback(snapshot.data()); + resolve2(); + }); + }); + + callback.should.be.calledWith(currentDataValue); + + await docRef.set(currentDataValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + // Assertions + + callback.should.be.calledOnce(); // Callback is not called again + + // Tear down + + unsubscribe(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('allows binding multiple callbacks to the same ref', () => { + return new Promise(async (resolve) => { + // Setup + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const currentDataValue = { name: 'doc1' }; + const newDataValue = { name: 'updated' }; + + const callbackA = sinon.spy(); + const callbackB = sinon.spy(); + + // Test + let unsubscribeA; + let unsubscribeB; + await new Promise((resolve2) => { + unsubscribeA = docRef.onSnapshot((snapshot) => { + callbackA(snapshot.data()); + resolve2(); + }); + }); + + await new Promise((resolve2) => { + unsubscribeB = docRef.onSnapshot((snapshot) => { + callbackB(snapshot.data()); + resolve2(); + }); + }); + + callbackA.should.be.calledWith(currentDataValue); + callbackA.should.be.calledOnce(); + + callbackB.should.be.calledWith(currentDataValue); + callbackB.should.be.calledOnce(); + + await docRef.set(newDataValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackA.should.be.calledWith(newDataValue); + callbackB.should.be.calledWith(newDataValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledTwice(); + + // Tear down + + unsubscribeA(); + unsubscribeB(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('listener stops listening when unsubscribed', () => { + return new Promise(async (resolve) => { + // Setup + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const currentDataValue = { name: 'doc1' }; + const newDataValue = { name: 'updated' }; + + const callbackA = sinon.spy(); + const callbackB = sinon.spy(); + + // Test + let unsubscribeA; + let unsubscribeB; + await new Promise((resolve2) => { + unsubscribeA = docRef.onSnapshot((snapshot) => { + callbackA(snapshot.data()); + resolve2(); + }); + }); + + await new Promise((resolve2) => { + unsubscribeB = docRef.onSnapshot((snapshot) => { + callbackB(snapshot.data()); + resolve2(); + }); + }); + + callbackA.should.be.calledWith(currentDataValue); + callbackA.should.be.calledOnce(); + + callbackB.should.be.calledWith(currentDataValue); + callbackB.should.be.calledOnce(); + + await docRef.set(newDataValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackA.should.be.calledWith(newDataValue); + callbackB.should.be.calledWith(newDataValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledTwice(); + + // Unsubscribe A + + unsubscribeA(); + + await docRef.set(currentDataValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackB.should.be.calledWith(currentDataValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledThrice(); + + // Unsubscribe B + + unsubscribeB(); + + await docRef.set(newDataValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledThrice(); + + resolve(); + }); + }); + }); + context('set()', () => { it('should create Document', () => { return firebase.native.firestore() From d40f464f1cb77ce809c8df0780c5b593c6671e7f Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Mon, 2 Oct 2017 15:45:07 +0100 Subject: [PATCH 11/21] [firestore][ios] Add document onSnapshot functionality --- ios/RNFirebase/RNFirebaseEvents.h | 4 + .../firestore/RNFirebaseFirestore.h | 3 + .../firestore/RNFirebaseFirestore.m | 78 +++++++++++++++++-- .../RNFirebaseFirestoreDocumentReference.h | 9 ++- .../RNFirebaseFirestoreDocumentReference.m | 59 +++++++++++++- 5 files changed, 144 insertions(+), 9 deletions(-) diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index bcdc186d..ffb3d369 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -16,6 +16,10 @@ static NSString *const DATABASE_CHILD_MODIFIED_EVENT = @"child_changed"; static NSString *const DATABASE_CHILD_REMOVED_EVENT = @"child_removed"; static NSString *const DATABASE_CHILD_MOVED_EVENT = @"child_moved"; +// Firestore +static NSString *const FIRESTORE_COLLECTION_SYNC_EVENT = @"firestore_collection_sync_event"; +static NSString *const FIRESTORE_DOCUMENT_SYNC_EVENT = @"firestore_document_sync_event"; + // Storage static NSString *const STORAGE_EVENT = @"storage_event"; static NSString *const STORAGE_ERROR = @"storage_error"; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.h b/ios/RNFirebase/firestore/RNFirebaseFirestore.h index af556283..e1cbcea6 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestore.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.h @@ -10,10 +10,13 @@ #import @interface RNFirebaseFirestore : RCTEventEmitter {} +@property NSMutableDictionary *collectionReferences; +@property NSMutableDictionary *documentReferences; + (void)promiseRejectException:(RCTPromiseRejectBlock)reject error:(NSError *)error; + (FIRFirestore *)getFirestoreForApp:(NSString *)appName; ++ (NSDictionary *)getJSError:(NSError *)nativeError; @end diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.m b/ios/RNFirebase/firestore/RNFirebaseFirestore.m index c15a894f..8d2b7c97 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestore.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.m @@ -13,7 +13,7 @@ RCT_EXPORT_MODULE(); - (id)init { self = [super init]; if (self != nil) { - + _documentReferences = [[NSMutableDictionary alloc] init]; } return self; } @@ -109,6 +109,24 @@ RCT_EXPORT_METHOD(documentGetAll:(NSString *) appName // Not supported on iOS out of the box } +RCT_EXPORT_METHOD(documentOffSnapshot:(NSString *) appName + path:(NSString *) path + listenerId:(nonnull NSNumber *) listenerId) { + RNFirebaseFirestoreDocumentReference *ref = [self getCachedDocumentForAppPath:appName path:path]; + [ref offSnapshot:listenerId]; + + if (![ref hasListeners]) { + [self clearCachedDocumentForAppPath:appName path:path]; + } +} + +RCT_EXPORT_METHOD(documentOnSnapshot:(NSString *) appName + path:(NSString *) path + listenerId:(nonnull NSNumber *) listenerId) { + RNFirebaseFirestoreDocumentReference *ref = [self getCachedDocumentForAppPath:appName path:path]; + [ref onSnapshot:listenerId]; +} + RCT_EXPORT_METHOD(documentSet:(NSString *) appName path:(NSString *) path data:(NSDictionary *) data @@ -130,10 +148,8 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName * INTERNALS/UTILS */ + (void)promiseRejectException:(RCTPromiseRejectBlock)reject error:(NSError *)error { - // TODO - // NSDictionary *jsError = [RNFirebaseDatabase getJSError:databaseError]; - // reject([jsError valueForKey:@"code"], [jsError valueForKey:@"message"], databaseError); - reject(@"TODO", [error description], error); + NSDictionary *jsError = [RNFirebaseFirestore getJSError:error]; + reject([jsError valueForKey:@"code"], [jsError valueForKey:@"message"], error); } + (FIRFirestore *)getFirestoreForApp:(NSString *)appName { @@ -145,12 +161,60 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName return [[RNFirebaseFirestoreCollectionReference alloc] initWithPathAndModifiers:appName path:path filters:filters orders:orders options:options]; } +- (RNFirebaseFirestoreDocumentReference *)getCachedDocumentForAppPath:(NSString *)appName path:(NSString *)path { + NSString *key = [NSString stringWithFormat:@"%@/%@", appName, path]; + RNFirebaseFirestoreDocumentReference *ref = _documentReferences[key]; + + if (ref == nil) { + ref = [self getDocumentForAppPath:appName path:path]; + _documentReferences[key] = ref; + } + return ref; +} + +- (void)clearCachedDocumentForAppPath:(NSString *)appName path:(NSString *)path { + NSString *key = [NSString stringWithFormat:@"%@/%@", appName, path]; + [_documentReferences removeObjectForKey:key]; +} + - (RNFirebaseFirestoreDocumentReference *)getDocumentForAppPath:(NSString *)appName path:(NSString *)path { - return [[RNFirebaseFirestoreDocumentReference alloc] initWithPath:appName path:path]; + return [[RNFirebaseFirestoreDocumentReference alloc] initWithPath:self app:appName path:path]; +} + +// TODO: Move to error util for use in other modules ++ (NSString *)getMessageWithService:(NSString *)message service:(NSString *)service fullCode:(NSString *)fullCode { + return [NSString stringWithFormat:@"%@: %@ (%@).", service, message, [fullCode lowercaseString]]; +} + ++ (NSString *)getCodeWithService:(NSString *)service code:(NSString *)code { + return [NSString stringWithFormat:@"%@/%@", [service lowercaseString], [code lowercaseString]]; +} + ++ (NSDictionary *)getJSError:(NSError *)nativeError { + NSMutableDictionary *errorMap = [[NSMutableDictionary alloc] init]; + [errorMap setValue:@(nativeError.code) forKey:@"nativeErrorCode"]; + [errorMap setValue:[nativeError localizedDescription] forKey:@"nativeErrorMessage"]; + + NSString *code; + NSString *message; + NSString *service = @"Firestore"; + + // TODO: Proper error codes + switch (nativeError.code) { + default: + code = [RNFirebaseFirestore getCodeWithService:service code:@"unknown"]; + message = [RNFirebaseFirestore getMessageWithService:@"An unknown error occurred." service:service fullCode:code]; + break; + } + + [errorMap setValue:code forKey:@"code"]; + [errorMap setValue:message forKey:@"message"]; + + return errorMap; } - (NSArray *)supportedEvents { - return @[DATABASE_SYNC_EVENT, DATABASE_TRANSACTION_EVENT]; + return @[FIRESTORE_COLLECTION_SYNC_EVENT, FIRESTORE_DOCUMENT_SYNC_EVENT]; } @end diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h index b4113c2c..421c3a1c 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h @@ -6,20 +6,27 @@ #if __has_include() #import +#import +#import "RNFirebaseEvents.h" #import "RNFirebaseFirestore.h" @interface RNFirebaseFirestoreDocumentReference : NSObject +@property RCTEventEmitter *emitter; @property NSString *app; @property NSString *path; @property FIRDocumentReference *ref; +@property NSMutableDictionary *listeners; -- (id)initWithPath:(NSString *)app path:(NSString *)path; +- (id)initWithPath:(RCTEventEmitter *)emitter app:(NSString *)app path:(NSString *)path; - (void)collections:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)create:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)delete:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (void)offSnapshot:(NSNumber *)listenerId; +- (void)onSnapshot:(NSNumber *)listenerId; - (void)set:(NSDictionary *)data options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)update:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; +- (BOOL)hasListeners; + (NSDictionary *)snapshotToDictionary:(FIRDocumentSnapshot *)documentSnapshot; @end diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index e7516ee2..e6ede48f 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -4,13 +4,16 @@ #if __has_include() -- (id)initWithPath:(NSString *) app +- (id)initWithPath:(RCTEventEmitter *)emitter + app:(NSString *) app path:(NSString *) path { self = [super init]; if (self) { + _emitter = emitter; _app = app; _path = path; _ref = [[RNFirebaseFirestore getFirestoreForApp:_app] documentWithPath:_path]; + _listeners = [[NSMutableDictionary alloc] init]; } return self; } @@ -46,6 +49,34 @@ }]; } +- (void)offSnapshot:(NSNumber *) listenerId { + id listener = _listeners[listenerId]; + if (listener) { + [_listeners removeObjectForKey:listenerId]; + [listener remove]; + } +} + +- (void)onSnapshot:(NSNumber *) listenerId { + if (_listeners[listenerId] == nil) { + id listenerBlock = ^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) { + if (error) { + id listener = _listeners[listenerId]; + if (listener) { + [_listeners removeObjectForKey:listenerId]; + [listener remove]; + } + [self handleDocumentSnapshotError:listenerId error:error]; + } else { + [self handleDocumentSnapshotEvent:listenerId documentSnapshot:snapshot]; + } + }; + + id listener = [_ref addSnapshotListener:listenerBlock]; + _listeners[listenerId] = listener; + } +} + - (void)set:(NSDictionary *) data options:(NSDictionary *) options resolver:(RCTPromiseResolveBlock) resolve @@ -69,6 +100,10 @@ }]; } +- (BOOL)hasListeners { + return [[_listeners allKeys] count] > 0; +} + + (void)handleWriteResponse:(NSError *) error resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject { @@ -95,6 +130,28 @@ return snapshot; } +- (void)handleDocumentSnapshotError:(NSNumber *)listenerId + error:(NSError *)error { + NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; + [event setValue:_app forKey:@"appName"]; + [event setValue:_path forKey:@"path"]; + [event setValue:listenerId forKey:@"listenerId"]; + [event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"]; + + [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; +} + +- (void)handleDocumentSnapshotEvent:(NSNumber *)listenerId + documentSnapshot:(FIRDocumentSnapshot *)documentSnapshot { + NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; + [event setValue:_app forKey:@"appName"]; + [event setValue:_path forKey:@"path"]; + [event setValue:listenerId forKey:@"listenerId"]; + [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"document"]; + + [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; +} + #endif @end From 22f7d77f54871ef9b722fb14eabf5152940ba99a Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 10:12:25 +0100 Subject: [PATCH 12/21] [firestore] Add collection `onSnapshot` support --- .../firestore/RNFirebaseFirestore.java | 60 ++-- ...NFirebaseFirestoreCollectionReference.java | 85 +++++- .../RNFirebaseFirestoreDocumentReference.java | 19 +- .../firestore/RNFirebaseFirestore.h | 2 - .../firestore/RNFirebaseFirestore.m | 52 ++-- .../RNFirebaseFirestoreCollectionReference.h | 7 +- .../RNFirebaseFirestoreCollectionReference.m | 59 +++- .../RNFirebaseFirestoreDocumentReference.h | 5 +- .../RNFirebaseFirestoreDocumentReference.m | 15 +- lib/modules/firestore/DocumentReference.js | 20 +- lib/modules/firestore/Query.js | 55 +++- lib/modules/firestore/QuerySnapshot.js | 2 +- lib/modules/firestore/index.js | 21 +- .../firestore/collectionReferenceTests.js | 262 ++++++++++++++++++ .../tests/firestore/documentReferenceTests.js | 2 + 15 files changed, 559 insertions(+), 107 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index 19c0f21b..180d286a 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -32,8 +32,6 @@ import io.invertase.firebase.Utils; public class RNFirebaseFirestore extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseFirestore"; - private HashMap collectionReferences = new HashMap<>(); - private HashMap documentReferences = new HashMap<>(); // private SparseArray transactionHandlers = new SparseArray<>(); RNFirebaseFirestore(ReactApplicationContext reactContext) { @@ -51,6 +49,20 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { ref.get(promise); } + @ReactMethod + public void collectionOffSnapshot(String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options, String listenerId) { + RNFirebaseFirestoreCollectionReference.offSnapshot(listenerId); + } + + @ReactMethod + public void collectionOnSnapshot(String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options, String listenerId) { + RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options); + ref.onSnapshot(listenerId); + } + + @ReactMethod public void documentBatch(final String appName, final ReadableArray writes, final ReadableMap commitOptions, final Promise promise) { @@ -134,18 +146,13 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { } @ReactMethod - public void documentOffSnapshot(String appName, String path, int listenerId) { - RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path); - ref.offSnapshot(listenerId); - - if (!ref.hasListeners()) { - clearCachedDocumentForAppPath(appName, path); - } + public void documentOffSnapshot(String appName, String path, String listenerId) { + RNFirebaseFirestoreDocumentReference.offSnapshot(listenerId); } @ReactMethod - public void documentOnSnapshot(String appName, String path, int listenerId) { - RNFirebaseFirestoreDocumentReference ref = getCachedDocumentForAppPath(appName, path); + public void documentOnSnapshot(String appName, String path, String listenerId) { + RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.onSnapshot(listenerId); } @@ -204,36 +211,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { ReadableArray filters, ReadableArray orders, ReadableMap options) { - return new RNFirebaseFirestoreCollectionReference(appName, path, filters, orders, options); - } - - /** - * Get a cached document reference for a specific app and path - * - * @param appName - * @param path - * @return - */ - private RNFirebaseFirestoreDocumentReference getCachedDocumentForAppPath(String appName, String path) { - String key = appName + "/" + path; - RNFirebaseFirestoreDocumentReference ref = documentReferences.get(key); - if (ref == null) { - ref = getDocumentForAppPath(appName, path); - documentReferences.put(key, ref); - } - return ref; - } - - /** - * Clear a cached document reference for a specific app and path - * - * @param appName - * @param path - * @return - */ - private void clearCachedDocumentForAppPath(String appName, String path) { - String key = appName + "/" + path; - documentReferences.remove(key); + return new RNFirebaseFirestoreCollectionReference(this.getReactApplicationContext(), appName, path, filters, orders, options); } /** diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java index 9cba86be..e8512d19 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java @@ -4,16 +4,21 @@ package io.invertase.firebase.firestore; import android.support.annotation.NonNull; import android.util.Log; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,21 +26,26 @@ import io.invertase.firebase.Utils; public class RNFirebaseFirestoreCollectionReference { private static final String TAG = "RNFSCollectionReference"; + private static Map collectionSnapshotListeners = new HashMap<>(); + private final String appName; private final String path; private final ReadableArray filters; private final ReadableArray orders; private final ReadableMap options; private final Query query; + private ReactContext reactContext; - RNFirebaseFirestoreCollectionReference(String appName, String path, ReadableArray filters, - ReadableArray orders, ReadableMap options) { + RNFirebaseFirestoreCollectionReference(ReactContext reactContext, String appName, String path, + ReadableArray filters, ReadableArray orders, + ReadableMap options) { this.appName = appName; this.path = path; this.filters = filters; this.orders = orders; this.options = options; this.query = buildQuery(); + this.reactContext = reactContext; } void get(final Promise promise) { @@ -54,6 +64,42 @@ public class RNFirebaseFirestoreCollectionReference { }); } + public static void offSnapshot(final String listenerId) { + ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + } + } + + public void onSnapshot(final String listenerId) { + if (!collectionSnapshotListeners.containsKey(listenerId)) { + final EventListener listener = new EventListener() { + @Override + public void onEvent(QuerySnapshot querySnapshot, FirebaseFirestoreException exception) { + if (exception == null) { + handleQuerySnapshotEvent(listenerId, querySnapshot); + } else { + ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + } + handleQuerySnapshotError(listenerId, exception); + } + } + }; + ListenerRegistration listenerRegistration = this.query.addSnapshotListener(listener); + collectionSnapshotListeners.put(listenerId, listenerRegistration); + } + } + + /* + * INTERNALS/UTILS + */ + + boolean hasListeners() { + return !collectionSnapshotListeners.isEmpty(); + } + private Query buildQuery() { Query query = RNFirebaseFirestore.getFirestoreForApp(appName).collection(path); query = applyFilters(query); @@ -134,4 +180,39 @@ public class RNFirebaseFirestoreCollectionReference { } return query; } + + /** + * Handles documentSnapshot events. + * + * @param listenerId + * @param querySnapshot + */ + private void handleQuerySnapshotEvent(String listenerId, QuerySnapshot querySnapshot) { + WritableMap event = Arguments.createMap(); + WritableMap data = FirestoreSerialize.snapshotToWritableMap(querySnapshot); + + event.putString("appName", appName); + event.putString("path", path); + event.putString("listenerId", listenerId); + event.putMap("querySnapshot", data); + + Utils.sendEvent(reactContext, "firestore_collection_sync_event", event); + } + + /** + * Handles a documentSnapshot error event + * + * @param listenerId + * @param exception + */ + private void handleQuerySnapshotError(String listenerId, FirebaseFirestoreException exception) { + WritableMap event = Arguments.createMap(); + + event.putString("appName", appName); + event.putString("path", path); + event.putString("listenerId", listenerId); + event.putMap("error", RNFirebaseFirestore.getJSError(exception)); + + Utils.sendEvent(reactContext, "firestore_collection_sync_event", event); + } } diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java index ffe442a0..8167f281 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java @@ -25,11 +25,12 @@ import io.invertase.firebase.Utils; public class RNFirebaseFirestoreDocumentReference { private static final String TAG = "RNFBFSDocumentReference"; + private static Map documentSnapshotListeners = new HashMap<>(); + private final String appName; private final String path; private ReactContext reactContext; private final DocumentReference ref; - private Map documentSnapshotListeners = new HashMap<>(); RNFirebaseFirestoreDocumentReference(ReactContext reactContext, String appName, String path) { this.appName = appName; @@ -79,14 +80,14 @@ public class RNFirebaseFirestoreDocumentReference { }); } - public void offSnapshot(final int listenerId) { + public static void offSnapshot(final String listenerId) { ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId); if (listenerRegistration != null) { listenerRegistration.remove(); } } - public void onSnapshot(final int listenerId) { + public void onSnapshot(final String listenerId) { if (!documentSnapshotListeners.containsKey(listenerId)) { final EventListener listener = new EventListener() { @Override @@ -154,7 +155,7 @@ public class RNFirebaseFirestoreDocumentReference { * INTERNALS/UTILS */ - public boolean hasListeners() { + boolean hasListeners() { return !documentSnapshotListeners.isEmpty(); } @@ -164,14 +165,14 @@ public class RNFirebaseFirestoreDocumentReference { * @param listenerId * @param documentSnapshot */ - private void handleDocumentSnapshotEvent(int listenerId, DocumentSnapshot documentSnapshot) { + private void handleDocumentSnapshotEvent(String listenerId, DocumentSnapshot documentSnapshot) { WritableMap event = Arguments.createMap(); WritableMap data = FirestoreSerialize.snapshotToWritableMap(documentSnapshot); event.putString("appName", appName); event.putString("path", path); - event.putInt("listenerId", listenerId); - event.putMap("document", data); + event.putString("listenerId", listenerId); + event.putMap("documentSnapshot", data); Utils.sendEvent(reactContext, "firestore_document_sync_event", event); } @@ -182,12 +183,12 @@ public class RNFirebaseFirestoreDocumentReference { * @param listenerId * @param exception */ - private void handleDocumentSnapshotError(int listenerId, FirebaseFirestoreException exception) { + private void handleDocumentSnapshotError(String listenerId, FirebaseFirestoreException exception) { WritableMap event = Arguments.createMap(); event.putString("appName", appName); event.putString("path", path); - event.putInt("listenerId", listenerId); + event.putString("listenerId", listenerId); event.putMap("error", RNFirebaseFirestore.getJSError(exception)); Utils.sendEvent(reactContext, "firestore_document_sync_event", event); diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.h b/ios/RNFirebase/firestore/RNFirebaseFirestore.h index e1cbcea6..4ecd5cae 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestore.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.h @@ -10,8 +10,6 @@ #import @interface RNFirebaseFirestore : RCTEventEmitter {} -@property NSMutableDictionary *collectionReferences; -@property NSMutableDictionary *documentReferences; + (void)promiseRejectException:(RCTPromiseRejectBlock)reject error:(NSError *)error; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.m b/ios/RNFirebase/firestore/RNFirebaseFirestore.m index 8d2b7c97..08c5c6dc 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestore.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.m @@ -13,7 +13,7 @@ RCT_EXPORT_MODULE(); - (id)init { self = [super init]; if (self != nil) { - _documentReferences = [[NSMutableDictionary alloc] init]; + } return self; } @@ -28,6 +28,25 @@ RCT_EXPORT_METHOD(collectionGet:(NSString *) appName [[self getCollectionForAppPath:appName path:path filters:filters orders:orders options:options] get:resolve rejecter:reject]; } +RCT_EXPORT_METHOD(collectionOffSnapshot:(NSString *) appName + path:(NSString *) path + filters:(NSArray *) filters + orders:(NSArray *) orders + options:(NSDictionary *) options + listenerId:(nonnull NSString *) listenerId) { + [RNFirebaseFirestoreCollectionReference offSnapshot:listenerId]; +} + +RCT_EXPORT_METHOD(collectionOnSnapshot:(NSString *) appName + path:(NSString *) path + filters:(NSArray *) filters + orders:(NSArray *) orders + options:(NSDictionary *) options + listenerId:(nonnull NSString *) listenerId) { + RNFirebaseFirestoreCollectionReference *ref = [self getCollectionForAppPath:appName path:path filters:filters orders:orders options:options]; + [ref onSnapshot:listenerId]; +} + RCT_EXPORT_METHOD(documentBatch:(NSString *) appName writes:(NSArray *) writes commitOptions:(NSDictionary *) commitOptions @@ -111,19 +130,14 @@ RCT_EXPORT_METHOD(documentGetAll:(NSString *) appName RCT_EXPORT_METHOD(documentOffSnapshot:(NSString *) appName path:(NSString *) path - listenerId:(nonnull NSNumber *) listenerId) { - RNFirebaseFirestoreDocumentReference *ref = [self getCachedDocumentForAppPath:appName path:path]; - [ref offSnapshot:listenerId]; - - if (![ref hasListeners]) { - [self clearCachedDocumentForAppPath:appName path:path]; - } + listenerId:(nonnull NSString *) listenerId) { + [RNFirebaseFirestoreDocumentReference offSnapshot:listenerId]; } RCT_EXPORT_METHOD(documentOnSnapshot:(NSString *) appName path:(NSString *) path - listenerId:(nonnull NSNumber *) listenerId) { - RNFirebaseFirestoreDocumentReference *ref = [self getCachedDocumentForAppPath:appName path:path]; + listenerId:(nonnull NSString *) listenerId) { + RNFirebaseFirestoreDocumentReference *ref = [self getDocumentForAppPath:appName path:path]; [ref onSnapshot:listenerId]; } @@ -158,23 +172,7 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName } - (RNFirebaseFirestoreCollectionReference *)getCollectionForAppPath:(NSString *)appName path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options { - return [[RNFirebaseFirestoreCollectionReference alloc] initWithPathAndModifiers:appName path:path filters:filters orders:orders options:options]; -} - -- (RNFirebaseFirestoreDocumentReference *)getCachedDocumentForAppPath:(NSString *)appName path:(NSString *)path { - NSString *key = [NSString stringWithFormat:@"%@/%@", appName, path]; - RNFirebaseFirestoreDocumentReference *ref = _documentReferences[key]; - - if (ref == nil) { - ref = [self getDocumentForAppPath:appName path:path]; - _documentReferences[key] = ref; - } - return ref; -} - -- (void)clearCachedDocumentForAppPath:(NSString *)appName path:(NSString *)path { - NSString *key = [NSString stringWithFormat:@"%@/%@", appName, path]; - [_documentReferences removeObjectForKey:key]; + return [[RNFirebaseFirestoreCollectionReference alloc] initWithPathAndModifiers:self app:appName path:path filters:filters orders:orders options:options]; } - (RNFirebaseFirestoreDocumentReference *)getDocumentForAppPath:(NSString *)appName path:(NSString *)path { diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h index 04c668dd..9bf003a2 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h @@ -5,10 +5,13 @@ #if __has_include() #import +#import +#import "RNFirebaseEvents.h" #import "RNFirebaseFirestore.h" #import "RNFirebaseFirestoreDocumentReference.h" @interface RNFirebaseFirestoreCollectionReference : NSObject +@property RCTEventEmitter *emitter; @property NSString *app; @property NSString *path; @property NSArray *filters; @@ -16,8 +19,10 @@ @property NSDictionary *options; @property FIRQuery *query; -- (id)initWithPathAndModifiers:(NSString *)app path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options; +- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter app:(NSString *)app path:(NSString *)path filters:(NSArray *)filters orders:(NSArray *)orders options:(NSDictionary *)options; - (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; ++ (void)offSnapshot:(NSString *)listenerId; +- (void)onSnapshot:(NSString *)listenerId; + (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot; @end diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m index 1b558963..70fb7ea6 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m @@ -4,13 +4,17 @@ #if __has_include() -- (id)initWithPathAndModifiers:(NSString *) app +static NSMutableDictionary *_listeners; + +- (id)initWithPathAndModifiers:(RCTEventEmitter *) emitter + app:(NSString *) app path:(NSString *) path filters:(NSArray *) filters orders:(NSArray *) orders options:(NSDictionary *) options { self = [super init]; if (self) { + _emitter = emitter; _app = app; _path = path; _filters = filters; @@ -18,6 +22,10 @@ _options = options; _query = [self buildQuery]; } + // Initialise the static listeners object if required + if (!_listeners) { + _listeners = [[NSMutableDictionary alloc] init]; + } return self; } @@ -33,6 +41,33 @@ }]; } ++ (void)offSnapshot:(NSString *) listenerId { + id listener = _listeners[listenerId]; + if (listener) { + [_listeners removeObjectForKey:listenerId]; + [listener remove]; + } +} + +- (void)onSnapshot:(NSString *) listenerId { + if (_listeners[listenerId] == nil) { + id listenerBlock = ^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) { + if (error) { + id listener = _listeners[listenerId]; + if (listener) { + [_listeners removeObjectForKey:listenerId]; + [listener remove]; + } + [self handleQuerySnapshotError:listenerId error:error]; + } else { + [self handleQuerySnapshotEvent:listenerId querySnapshot:snapshot]; + } + }; + id listener = [_query addSnapshotListener:listenerBlock]; + _listeners[listenerId] = listener; + } +} + - (FIRQuery *)buildQuery { FIRQuery *query = (FIRQuery*)[[RNFirebaseFirestore getFirestoreForApp:_app] collectionWithPath:_path]; query = [self applyFilters:query]; @@ -96,6 +131,28 @@ return query; } +- (void)handleQuerySnapshotError:(NSString *)listenerId + error:(NSError *)error { + NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; + [event setValue:_app forKey:@"appName"]; + [event setValue:_path forKey:@"path"]; + [event setValue:listenerId forKey:@"listenerId"]; + [event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"]; + + [_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; +} + +- (void)handleQuerySnapshotEvent:(NSString *)listenerId + querySnapshot:(FIRQuerySnapshot *)querySnapshot { + NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; + [event setValue:_app forKey:@"appName"]; + [event setValue:_path forKey:@"path"]; + [event setValue:listenerId forKey:@"listenerId"]; + [event setValue:[RNFirebaseFirestoreCollectionReference snapshotToDictionary:querySnapshot] forKey:@"querySnapshot"]; + + [_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; +} + + (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot { NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; [snapshot setValue:[self documentChangesToArray:querySnapshot.documentChanges] forKey:@"changes"]; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h index 421c3a1c..eac466f5 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h @@ -15,15 +15,14 @@ @property NSString *app; @property NSString *path; @property FIRDocumentReference *ref; -@property NSMutableDictionary *listeners; - (id)initWithPath:(RCTEventEmitter *)emitter app:(NSString *)app path:(NSString *)path; - (void)collections:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)create:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)delete:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)get:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; -- (void)offSnapshot:(NSNumber *)listenerId; -- (void)onSnapshot:(NSNumber *)listenerId; ++ (void)offSnapshot:(NSString *)listenerId; +- (void)onSnapshot:(NSString *)listenerId; - (void)set:(NSDictionary *)data options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)update:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (BOOL)hasListeners; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index e6ede48f..cb56e86e 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -4,6 +4,8 @@ #if __has_include() +static NSMutableDictionary *_listeners; + - (id)initWithPath:(RCTEventEmitter *)emitter app:(NSString *) app path:(NSString *) path { @@ -13,6 +15,9 @@ _app = app; _path = path; _ref = [[RNFirebaseFirestore getFirestoreForApp:_app] documentWithPath:_path]; + } + // Initialise the static listeners object if required + if (!_listeners) { _listeners = [[NSMutableDictionary alloc] init]; } return self; @@ -49,7 +54,7 @@ }]; } -- (void)offSnapshot:(NSNumber *) listenerId { ++ (void)offSnapshot:(NSString *) listenerId { id listener = _listeners[listenerId]; if (listener) { [_listeners removeObjectForKey:listenerId]; @@ -57,7 +62,7 @@ } } -- (void)onSnapshot:(NSNumber *) listenerId { +- (void)onSnapshot:(NSString *) listenerId { if (_listeners[listenerId] == nil) { id listenerBlock = ^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) { if (error) { @@ -130,7 +135,7 @@ return snapshot; } -- (void)handleDocumentSnapshotError:(NSNumber *)listenerId +- (void)handleDocumentSnapshotError:(NSString *)listenerId error:(NSError *)error { NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; [event setValue:_app forKey:@"appName"]; @@ -141,13 +146,13 @@ [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } -- (void)handleDocumentSnapshotEvent:(NSNumber *)listenerId +- (void)handleDocumentSnapshotEvent:(NSString *)listenerId documentSnapshot:(FIRDocumentSnapshot *)documentSnapshot { NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; [event setValue:_app forKey:@"appName"]; [event setValue:_path forKey:@"path"]; [event setValue:listenerId forKey:@"listenerId"]; - [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"document"]; + [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"documentSnapshot"]; [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js index a061ca18..9873c2cd 100644 --- a/lib/modules/firestore/DocumentReference.js +++ b/lib/modules/firestore/DocumentReference.js @@ -6,6 +6,7 @@ import CollectionReference from './CollectionReference'; import DocumentSnapshot from './DocumentSnapshot'; import Path from './Path'; import INTERNALS from './../../internals'; +import { firestoreAutoId } from '../../utils'; export type DeleteOptions = { lastUpdateTime?: string, @@ -19,9 +20,6 @@ export type WriteResult = { writeTime: string, } -// track all event registrations -let listeners = 0; - /** * @class DocumentReference */ @@ -94,12 +92,17 @@ export default class DocumentReference { onSnapshot(onNext: Function, onError?: Function): () => void { // TODO: Validation - const listenerId = listeners++; + const listenerId = firestoreAutoId(); + + const listener = (nativeDocumentSnapshot) => { + const documentSnapshot = new DocumentSnapshot(this, nativeDocumentSnapshot); + onNext(documentSnapshot); + }; // Listen to snapshot events this._firestore.on( this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), - onNext, + listener, ); // Listen for snapshot error events @@ -115,7 +118,7 @@ export default class DocumentReference { .documentOnSnapshot(this.path, listenerId); // Return an unsubscribe method - return this._offDocumentSnapshot.bind(this, listenerId, onNext); + return this._offDocumentSnapshot.bind(this, listenerId, listener); } set(data: { [string]: any }, writeOptions?: WriteOptions): Promise { @@ -130,11 +133,14 @@ export default class DocumentReference { } /** - * Remove auth change listener + * Remove document snapshot listener * @param listener */ _offDocumentSnapshot(listenerId: number, listener: Function) { this._firestore.log.info('Removing onDocumentSnapshot listener'); this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshot:${listenerId}`), listener); + this._firestore.removeListener(this._firestore._getAppEventName(`onDocumentSnapshotError:${listenerId}`), listener); + this._firestore._native + .documentOffSnapshot(this.path, listenerId); } } diff --git a/lib/modules/firestore/Query.js b/lib/modules/firestore/Query.js index e415ff0b..c4f271f5 100644 --- a/lib/modules/firestore/Query.js +++ b/lib/modules/firestore/Query.js @@ -5,7 +5,8 @@ import DocumentSnapshot from './DocumentSnapshot'; import Path from './Path'; import QuerySnapshot from './QuerySnapshot'; -import INTERNALS from './../../internals'; +import INTERNALS from '../../internals'; +import { firestoreAutoId } from '../../utils'; const DIRECTIONS = { ASC: 'ASCENDING', @@ -51,6 +52,7 @@ export default class Query { _fieldFilters: FieldFilter[]; _fieldOrders: FieldOrder[]; _firestore: Object; + _iid: number; _queryOptions: QueryOptions; _referencePath: Path; @@ -128,7 +130,40 @@ export default class Query { } onSnapshot(onNext: () => any, onError?: () => any): () => void { + // TODO: Validation + const listenerId = firestoreAutoId(); + const listener = (nativeQuerySnapshot) => { + const querySnapshot = new QuerySnapshot(this._firestore, this, nativeQuerySnapshot); + onNext(querySnapshot); + }; + + // Listen to snapshot events + this._firestore.on( + this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), + listener, + ); + + // Listen for snapshot error events + if (onError) { + this._firestore.on( + this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), + onError, + ); + } + + // Add the native listener + this._firestore._native + .collectionOnSnapshot( + this._referencePath.relativeName, + this._fieldFilters, + this._fieldOrders, + this._queryOptions, + listenerId + ); + + // Return an unsubscribe method + return this._offCollectionSnapshot.bind(this, listenerId, listener); } orderBy(fieldPath: string, directionStr?: Direction = 'asc'): Query { @@ -216,4 +251,22 @@ export default class Query { return new Query(this.firestore, this._referencePath, combinedFilters, this._fieldOrders, this._queryOptions); } + + /** + * Remove query snapshot listener + * @param listener + */ + _offCollectionSnapshot(listenerId: number, listener: Function) { + this._firestore.log.info('Removing onQuerySnapshot listener'); + this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), listener); + this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), listener); + this._firestore._native + .collectionOffSnapshot( + this._referencePath.relativeName, + this._fieldFilters, + this._fieldOrders, + this._queryOptions, + listenerId + ); + } } diff --git a/lib/modules/firestore/QuerySnapshot.js b/lib/modules/firestore/QuerySnapshot.js index c1a6b035..4b3f4b3e 100644 --- a/lib/modules/firestore/QuerySnapshot.js +++ b/lib/modules/firestore/QuerySnapshot.js @@ -59,7 +59,7 @@ export default class QuerySnapshot { // TODO: Validation // validate.isFunction('callback', callback); - for (const doc of this.docs) { + for (const doc of this._docs) { callback(doc); } } diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js index e50f0482..4f1ae343 100644 --- a/lib/modules/firestore/index.js +++ b/lib/modules/firestore/index.js @@ -16,11 +16,19 @@ import INTERNALS from './../../internals'; const unquotedIdentifier_ = '(?:[A-Za-z_][A-Za-z_0-9]*)'; const UNQUOTED_IDENTIFIER_REGEX = new RegExp(`^${unquotedIdentifier_}$`); +type CollectionSyncEvent = { + appName: string, + querySnapshot?: QuerySnapshot, + error?: Object, + listenerId: string, + path: string, +} + type DocumentSyncEvent = { appName: string, - document?: DocumentSnapshot, + documentSnapshot?: DocumentSnapshot, error?: Object, - listenerId: number, + listenerId: string, path: string, } @@ -139,11 +147,11 @@ export default class Firestore extends ModuleBase { * @param event * @private */ - _onCollectionSyncEvent(event: DocumentSyncEvent) { + _onCollectionSyncEvent(event: CollectionSyncEvent) { if (event.error) { - this.emit(this._getAppEventName(`onCollectionSnapshotError:${event.listenerId}`, event.error)); + this.emit(this._getAppEventName(`onQuerySnapshotError:${event.listenerId}`), event.error); } else { - this.emit(this._getAppEventName(`onCollectionSnapshot:${event.listenerId}`, event.document)); + this.emit(this._getAppEventName(`onQuerySnapshot:${event.listenerId}`), event.querySnapshot); } } @@ -156,8 +164,7 @@ export default class Firestore extends ModuleBase { if (event.error) { this.emit(this._getAppEventName(`onDocumentSnapshotError:${event.listenerId}`), event.error); } else { - const snapshot = new DocumentSnapshot(this, event.document); - this.emit(this._getAppEventName(`onDocumentSnapshot:${event.listenerId}`), snapshot); + this.emit(this._getAppEventName(`onDocumentSnapshot:${event.listenerId}`), event.documentSnapshot); } } } diff --git a/tests/src/tests/firestore/collectionReferenceTests.js b/tests/src/tests/firestore/collectionReferenceTests.js index d0ba525d..ab2a1f92 100644 --- a/tests/src/tests/firestore/collectionReferenceTests.js +++ b/tests/src/tests/firestore/collectionReferenceTests.js @@ -1,3 +1,5 @@ +import sinon from 'sinon'; +import 'should-sinon'; import should from 'should'; function collectionReferenceTests({ describe, it, context, firebase }) { @@ -50,6 +52,266 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); + context('onSnapshot()', () => { + it('calls callback with the initial data and then when document changes', () => { + return new Promise(async (resolve) => { + const collectionRef = firebase.native.firestore().collection('document-tests'); + const currentDocValue = { name: 'doc1' }; + const newDocValue = { name: 'updated' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise((resolve2) => { + unsubscribe = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callback(doc.data())); + resolve2(); + }); + }); + + callback.should.be.calledWith(currentDocValue); + + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + await docRef.set(newDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + // Assertions + + callback.should.be.calledWith(newDocValue); + callback.should.be.calledTwice(); + + // Tear down + + unsubscribe(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('calls callback with the initial data and then when document is added', () => { + return new Promise(async (resolve) => { + const collectionRef = firebase.native.firestore().collection('document-tests'); + const currentDocValue = { name: 'doc1' }; + const newDocValue = { name: 'updated' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise((resolve2) => { + unsubscribe = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callback(doc.data())); + resolve2(); + }); + }); + + callback.should.be.calledWith(currentDocValue); + + const docRef = firebase.native.firestore().doc('document-tests/doc2'); + await docRef.set(newDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + // Assertions + + callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(newDocValue); + callback.should.be.calledThrice(); + + // Tear down + + unsubscribe(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('doesn\'t call callback when the ref is updated with the same value', async () => { + return new Promise(async (resolve) => { + const collectionRef = firebase.native.firestore().collection('document-tests'); + const currentDocValue = { name: 'doc1' }; + + const callback = sinon.spy(); + + // Test + + let unsubscribe; + await new Promise((resolve2) => { + unsubscribe = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callback(doc.data())); + resolve2(); + }); + }); + + callback.should.be.calledWith(currentDocValue); + + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + await docRef.set(currentDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + // Assertions + + callback.should.be.calledOnce(); // Callback is not called again + + // Tear down + + unsubscribe(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('allows binding multiple callbacks to the same ref', () => { + return new Promise(async (resolve) => { + // Setup + const collectionRef = firebase.native.firestore().collection('document-tests'); + const currentDocValue = { name: 'doc1' }; + const newDocValue = { name: 'updated' }; + + const callbackA = sinon.spy(); + const callbackB = sinon.spy(); + + // Test + let unsubscribeA; + let unsubscribeB; + await new Promise((resolve2) => { + unsubscribeA = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callbackA(doc.data())); + resolve2(); + }); + }); + await new Promise((resolve2) => { + unsubscribeB = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callbackB(doc.data())); + resolve2(); + }); + }); + + callbackA.should.be.calledWith(currentDocValue); + callbackA.should.be.calledOnce(); + + callbackB.should.be.calledWith(currentDocValue); + callbackB.should.be.calledOnce(); + + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + await docRef.set(newDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackA.should.be.calledWith(newDocValue); + callbackB.should.be.calledWith(newDocValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledTwice(); + + // Tear down + + unsubscribeA(); + unsubscribeB(); + + resolve(); + }); + }); + }); + + context('onSnapshot()', () => { + it('listener stops listening when unsubscribed', () => { + return new Promise(async (resolve) => { + // Setup + const collectionRef = firebase.native.firestore().collection('document-tests'); + const currentDocValue = { name: 'doc1' }; + const newDocValue = { name: 'updated' }; + + const callbackA = sinon.spy(); + const callbackB = sinon.spy(); + + // Test + let unsubscribeA; + let unsubscribeB; + await new Promise((resolve2) => { + unsubscribeA = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callbackA(doc.data())); + resolve2(); + }); + }); + await new Promise((resolve2) => { + unsubscribeB = collectionRef.onSnapshot((snapshot) => { + snapshot.forEach(doc => callbackB(doc.data())); + resolve2(); + }); + }); + + callbackA.should.be.calledWith(currentDocValue); + callbackA.should.be.calledOnce(); + + callbackB.should.be.calledWith(currentDocValue); + callbackB.should.be.calledOnce(); + + const docRef = firebase.native.firestore().doc('document-tests/doc1'); + await docRef.set(newDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackA.should.be.calledWith(newDocValue); + callbackB.should.be.calledWith(newDocValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledTwice(); + + // Unsubscribe A + + unsubscribeA(); + + await docRef.set(currentDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackB.should.be.calledWith(currentDocValue); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledThrice(); + + // Unsubscribe B + + unsubscribeB(); + + await docRef.set(newDocValue); + + await new Promise((resolve2) => { + setTimeout(() => resolve2(), 5); + }); + + callbackA.should.be.calledTwice(); + callbackB.should.be.calledThrice(); + + resolve(); + }); + }); + }); + // Where context('where()', () => { it('correctly handles == boolean values', () => { diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js index f8b9afd6..11166366 100644 --- a/tests/src/tests/firestore/documentReferenceTests.js +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -49,6 +49,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { callback.should.be.calledWith(currentDataValue); + // Update the document + await docRef.set(newDataValue); await new Promise((resolve2) => { From 5e3ac2491d92529b0d46fdf1fe9277c3feb27612 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 12:03:16 +0100 Subject: [PATCH 13/21] [docs] Add some basic Firestore docs for launch --- README.md | 1 + docs/README.md | 1 + docs/_sidebar.md | 1 + docs/installation-android.md | 3 + docs/installation-ios.md | 1 + docs/modules/firestore.md | 171 +++++++++++++++++++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 docs/modules/firestore.md diff --git a/README.md b/README.md index 2e4d5b03..49839f82 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a | **Cloud Messaging (FCM)** | ✅ | ✅ | ✅ |**?**| | **Crash Reporting** | ✅ | ✅ | ✅ | ❌ | | **Dynamic Links** | ❌ | ❌ | ❌ | ❌ | +| **Firestore** | ❌ | ❌ | ✅ | ❌ | | **Invites** | ❌ | ❌ | ❌ | ❌ | | **Performance Monitoring** | ✅ | ✅ | ✅ | ❌ | | **Realtime Database** | ✅ | ✅ | ✅ | ✅ | diff --git a/docs/README.md b/docs/README.md index 9f81e70f..787f6268 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,6 +46,7 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a | **Cloud Messaging (FCM)** | ✅ | ✅ | ✅ |**?**| | **Crash Reporting** | ✅ | ✅ | ✅ | ❌ | | **Dynamic Links** | ❌ | ❌ | ❌ | ❌ | +| **Firestore** | ❌ | ❌ | ✅ | ❌ | | **Invites** | ❌ | ❌ | ❌ | ❌ | | **Performance Monitoring** | ✅ | ✅ | ✅ | ❌ | | **Realtime Database** | ✅ | ✅ | ✅ | ✅ | diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 6caea69d..618f8daf 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -24,6 +24,7 @@ - [Cloud Messaging](/modules/cloud-messaging) - [Crash Reporting](/modules/crash) - [Database](/modules/database) + - [Firestore (Beta)](/modules/firestore) - [Remote Config](/modules/config) - [Storage](/modules/storage) - [Transactions](/modules/transactions) diff --git a/docs/installation-android.md b/docs/installation-android.md index c26d9643..2fb83fc2 100644 --- a/docs/installation-android.md +++ b/docs/installation-android.md @@ -53,6 +53,7 @@ dependencies { compile "com.google.firebase:firebase-config:11.2.0" compile "com.google.firebase:firebase-crash:11.2.0" compile "com.google.firebase:firebase-database:11.2.0" + compile "com.google.firebase:firebase-firestore:11.2.0" compile "com.google.firebase:firebase-messaging:11.2.0" compile "com.google.firebase:firebase-perf:11.2.0" compile "com.google.firebase:firebase-storage:11.2.0" @@ -88,6 +89,7 @@ import io.invertase.firebase.auth.RNFirebaseAuthPackage; // Firebase Auth import io.invertase.firebase.config.RNFirebaseRemoteConfigPackage; // Firebase Remote Config import io.invertase.firebase.crash.RNFirebaseCrashPackage; // Firebase Crash Reporting import io.invertase.firebase.database.RNFirebaseDatabasePackage; // Firebase Realtime Database +import io.invertase.firebase.firestore.RNFirebaseFirestorePackage; // Firebase Firestore import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; // Firebase Cloud Messaging import io.invertase.firebase.perf.RNFirebasePerformancePackage; // Firebase Performance import io.invertase.firebase.storage.RNFirebaseStoragePackage; // Firebase Storage @@ -107,6 +109,7 @@ public class MainApplication extends Application implements ReactApplication { new RNFirebaseRemoteConfigPackage(), new RNFirebaseCrashPackage(), new RNFirebaseDatabasePackage(), + new RNFirebaseFirestorePackage(), new RNFirebaseMessagingPackage(), new RNFirebasePerformancePackage(), new RNFirebaseStoragePackage() diff --git a/docs/installation-ios.md b/docs/installation-ios.md index 4c595ccf..ff6b2e9b 100644 --- a/docs/installation-ios.md +++ b/docs/installation-ios.md @@ -69,6 +69,7 @@ pod 'Firebase/Auth' pod 'Firebase/Crash' pod 'Firebase/Database' pod 'Firebase/DynamicLinks' +pod 'Firestore' pod 'Firebase/Messaging' pod 'Firebase/RemoteConfig' pod 'Firebase/Storage' diff --git a/docs/modules/firestore.md b/docs/modules/firestore.md new file mode 100644 index 00000000..edd809ec --- /dev/null +++ b/docs/modules/firestore.md @@ -0,0 +1,171 @@ + +# Firestore (Beta) + +RNFirebase mimics the [Firestore Web SDK](https://firebase.google.com/docs/database/web/read-and-write), whilst +providing support for devices in low/no data connection state. + +All Firestore operations are accessed via `firestore()`. + +Please note that Persistence (offline support) is enabled by default with Firestore on iOS and Android. + +## Add and Manage Data + +### Collections + +Read information about a collection example: +```javascript +firebase.firestore() + .collection('posts') + .get() + .then(querySnapshot => { + // Access all the documents in the collection + const docs = querySnapshot.docs; + // Access the list of document changes for the collection + const changes = querySnapshot.docChanges; + // Loop through the documents + querySnapshot.forEach((doc) => { + const value = doc.data(); + }) + }) +``` + +Add to a collection example (generated ID): +```javascript +firebase.firestore() + .collection('posts') + .add({ + title: 'Amazing post', + }) + .then(() => { + // Document added to collection and ID generated + // Will have path: `posts/{generatedId}` + }) +``` + +Add to a collection example (manual ID): +```javascript +firebase.firestore() + .collection('posts') + .doc('post1') + .set({ + title: 'My awesome post', + content: 'Some awesome content', + }) + .then(() => { + // Document added to collection with path: `posts/post1` + }) +``` + +### Documents + +There are multiple ways to read a document. The following are equivalent examples: +```javascript +firebase.firestore() + .doc('posts/posts1') + .get((documentSnapshot) => { + const value = documentSnapshot.data(); + }); + +firebase.firestore() + .collection('posts') + .doc('posts1') + .get((documentSnapshot) => { + const value = documentSnapshot.data(); + }); +``` + +Create a document example: +```javascript +firebase.firestore() + .doc('posts/posts1') + .set({ + title: 'My awesome post', + content: 'Some awesome content', + }) + .then(() => { + // Document created + }); +``` + +Updating a document example: +```javascript +firebase.firestore() + .doc('posts/posts1') + .update({ + title: 'My awesome post', + }) + .then(() => { + // Document created + }); +``` + +Deleting a document example: +```javascript +firebase.firestore() + .doc('posts/posts1') + .delete() + .then(() => { + // Document deleted + }); +``` + +### Batching document updates + +Writes, updates and deletes to documents can be batched and committed atomically as follows: + +```javascript +const ayRef = firebase.firestore().doc('places/AY'); +const lRef = firebase.firestore().doc('places/LON'); +const nycRef = firebase.firestore().doc('places/NYC'); +const sfRef = firebase.firestore().doc('places/SF'); + +firebase.firestore() + .batch() + .set(ayRef, { name: 'Aylesbury' }) + .set(lRef, { name: 'London' }) + .set(nycRef, { name: 'New York City' }) + .set(sfRef, { name: 'San Francisco' }) + .update(nycRef, { population: 1000000 }) + .update(sfRef, { name: 'San Fran' }) + .set(lRef, { population: 3000000 }, { merge: true }) + .delete(ayRef) + .commit() + .then(() => { + // Would end up with three documents in the collection: London, New York City and San Francisco + }); +``` + +### Transactions + +Coming soon + +## Realtime Updates + +### Collections + +Listen to collection updates example: +```javascript +firebase.firestore() + .collection('cities') + .where('state', '==', 'CA') + .onSnapshot((querySnapshot) => { + querySnapshot.forEach((doc) => { + // DocumentSnapshot available + }) + }) +``` + +The snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified). + +### Documents + +Listen to document updates example: +```javascript +firebase.firestore() + .doc('posts/post1') + .onSnapshot((documentSnapshot) => { + // DocumentSnapshot available + }) +``` + +The snapshot handler will receive the current contents of the document, and any subsequent changes to the document. From c1ecf22a38d43812de39877e1c1ca44ae9beced1 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 15:54:03 +0100 Subject: [PATCH 14/21] Fix NetInfo RN 0.49 deprecation --- tests/src/containers/CoreContainer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/containers/CoreContainer.js b/tests/src/containers/CoreContainer.js index 57a786e8..fdf30041 100644 --- a/tests/src/containers/CoreContainer.js +++ b/tests/src/containers/CoreContainer.js @@ -31,7 +31,7 @@ class CoreContainer extends React.Component { NetInfo.isConnected.fetch().then((isConnected) => { this.handleAppStateChange('active'); // Force connect (react debugger issue) this.props.dispatch(setNetworkState(isConnected)); - NetInfo.isConnected.addEventListener('change', this.handleNetworkChange); + NetInfo.isConnected.addEventListener('connectionChange', this.handleNetworkChange); }); } @@ -40,7 +40,7 @@ class CoreContainer extends React.Component { */ componentWillUnmount() { AppState.removeEventListener('change', this.handleAppStateChange); - NetInfo.isConnected.removeEventListener('change', this.handleNetworkChange); + NetInfo.isConnected.removeEventListener('connectionChange', this.handleNetworkChange); } props: Props; From d94abaa77c4f4a3eae25db5d0a66ec48a0d1fbbd Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 16:02:13 +0100 Subject: [PATCH 15/21] Re-run react-native link to check all packages are installed correctly --- .../app/src/main/assets/fonts/Feather.ttf | Bin 0 -> 41648 bytes .../assets/fonts/MaterialCommunityIcons.ttf | Bin 245676 -> 292556 bytes .../app/src/main/assets/fonts/Octicons.ttf | Bin 27428 -> 27512 bytes .../project.pbxproj | 5 ++++- tests/ios/ReactNativeFirebaseDemo/Info.plist | 3 ++- 5 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 tests/android/app/src/main/assets/fonts/Feather.ttf diff --git a/tests/android/app/src/main/assets/fonts/Feather.ttf b/tests/android/app/src/main/assets/fonts/Feather.ttf new file mode 100644 index 0000000000000000000000000000000000000000..97e3b0faefb652e5d0b24f933546fbbe8302580d GIT binary patch literal 41648 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%0A)Z$rsd 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 delta 10849 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 W`p&L%W_wI@*V>iNU-H?2zyAXulAgr? diff --git a/tests/android/app/src/main/assets/fonts/Octicons.ttf b/tests/android/app/src/main/assets/fonts/Octicons.ttf index 5b1f7d6ea2c870df4f29f4b40a300334844d2214..09e2b2d79c370d9370a772b3542acdfca66ed634 100644 GIT binary patch delta 905 zcmZ8gZAep57=F*4-c5J+k=|{t-Ra%;-RAXUh^B~$*^(%f60@4==2n(Y#Z)XRhx8}u zk6wR93=;_%CQc(FK}1BN=tm`DAt)lzj|eG|5bZb>Ot|O1&+~pf_dVylcW41^Eg%;l z0FXflL{Q;&S6&ERo^k**da#!%zm(}J->UftfT}UCs}FixSBLssmhMVSB{!Sy-k2Gj z-~h-bfNNt70k5B0`MQZ?-|)(9zz)$*g2d%C=H`ZAd)MPa;uPku@!D!?srN>7GdLcP z!>B3f?P^6bREsgr0k+v23`}*btONKM0K(YXa;M$)Tx$k!_=^F=bgXyWf5nqCDYc5i zKOiM=9l)2>4^xM>ALWV9#u6umB24lnzn_R3|k`t;rZ!o9wNekh|qG^1q4#MMSZnEK&}iV3MYjmXfxUx#a7~ zy~%HAiY}p_(aS1{s!G+aT2oW%9QAGWwx&)quGvZ9Ql_<~+K_hsK6d3LqMvcYBYU7|O z(d0Ibn0Cy?<^l67^MXZf@mmJ0jJ3`B!n$aa*h01qE|JUO?sMaI!d_wTv5(p}9Ty!V zjtys#^PIEF>2o$a$I{94S>C}n^AUcIUxa9o4Aaqoj?;9D@F9j#7@x2h6rjOZYP9C$ zy41A#m@VZPypcyo-eJuDf3Vh`9v&kUKGajj)JP+F%+Sh#m delta 710 zcmexyjd95}Mk5AB1_lORh6V;^h5$FW5Z|gV%{mMW*&IMo8UJ8?BmW<<=NK3mBY^yb z@h>m1!)UKd&+{u;nl?L}z5ACZ=$?vXlYk?f|hFNPzhr6FX45 z2#~Lmky}#HZNp>%tQx8@+ zd}Clpyu!f1BwvtUT#|nD#$^Tut>iBZ4D4KBw*%ep^Zm1ZJipCX25u&xJ_d#>QxgBe z=t+#`>>gl+TPE8v#xkzoJcm(@aq>PUcSeQD4$R_{6_`0DYcg*EO59_f$XH*)wU6r? zw+nX-_ag3dJZU_Ocy93u@Otph<9)%W$5+Jnj$e&GjsJ!KlR%chB|#6NH^OYf8p0vM zUBcUhpNUwAAinvkQI)>BGQ$&a;4v_;FmNzRLD{Sf0*tOuHXBepV{%!NuDTn;9AGM9WZ?v= z6=39mvRN4v7?q)HHlTRecNmgez)is=aKDltRV~PUfmd!~i8H}6H fq&Bdsfg+HB@&A7YP@IAiDZ|Cn`wc-fa`XZKD@?hJ diff --git a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj index 5db1f6f4..24ef39de 100644 --- a/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj +++ b/tests/ios/ReactNativeFirebaseDemo.xcodeproj/project.pbxproj @@ -5,7 +5,6 @@ }; objectVersion = 46; objects = { - /* Begin PBXBuildFile section */ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; @@ -37,6 +36,7 @@ E51DA6317685417F97A59475 /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C76E33ACF004369AEB318B1 /* libRNVectorIcons.a */; }; EA30CACE4CB84AC1BAE59432 /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4935BEDE99B9436581953E77 /* Entypo.ttf */; }; F57EC9E3C5414A99821B73F4 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = DB5BB32AF70B41678974C6B4 /* EvilIcons.ttf */; }; + 58EAF3FA628941C98A02BAE4 /* Feather.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 528FC2DCD44F4567A3FE9F59 /* Feather.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -322,6 +322,7 @@ CC24EB30F0484352BD65FFC1 /* Octicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Octicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Octicons.ttf"; sourceTree = ""; }; DB5BB32AF70B41678974C6B4 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; }; FD3DFC8253C74B6298AFD3B7 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; }; + 528FC2DCD44F4567A3FE9F59 /* Feather.ttf */ = {isa = PBXFileReference; name = "Feather.ttf"; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -597,6 +598,7 @@ CC24EB30F0484352BD65FFC1 /* Octicons.ttf */, FD3DFC8253C74B6298AFD3B7 /* SimpleLineIcons.ttf */, 182271FFECD74C3B92960E1D /* Zocial.ttf */, + 528FC2DCD44F4567A3FE9F59 /* Feather.ttf */, ); name = Resources; sourceTree = ""; @@ -990,6 +992,7 @@ 885057F5D1FA461AAAE0B487 /* Octicons.ttf in Resources */, 42A0E8F428A74B23B4C7D95A /* SimpleLineIcons.ttf in Resources */, D46EBD0604CE40EFB18F8A35 /* Zocial.ttf in Resources */, + 58EAF3FA628941C98A02BAE4 /* Feather.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/tests/ios/ReactNativeFirebaseDemo/Info.plist b/tests/ios/ReactNativeFirebaseDemo/Info.plist index f764dd7f..0ebc846d 100644 --- a/tests/ios/ReactNativeFirebaseDemo/Info.plist +++ b/tests/ios/ReactNativeFirebaseDemo/Info.plist @@ -34,7 +34,7 @@ NSLocationWhenInUseUsageDescription - + UIAppFonts Entypo.ttf @@ -47,6 +47,7 @@ Octicons.ttf SimpleLineIcons.ttf Zocial.ttf + Feather.ttf UILaunchStoryboardName LaunchScreen From f68c6d168e87e8b2128f65be7d89915d62d6a6a5 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 16:02:41 +0100 Subject: [PATCH 16/21] [android] Remove backwards compatibility for 0.47.0 --- .../io/invertase/firebase/RNFirebasePackage.java | 12 ------------ .../firebase/admob/RNFirebaseAdMobPackage.java | 12 ------------ .../analytics/RNFirebaseAnalyticsPackage.java | 12 ------------ .../firebase/auth/RNFirebaseAuthPackage.java | 12 ------------ .../config/RNFirebaseRemoteConfigPackage.java | 12 ------------ .../firebase/crash/RNFirebaseCrashPackage.java | 12 ------------ .../firebase/database/RNFirebaseDatabasePackage.java | 12 ------------ .../firestore/RNFirebaseFirestorePackage.java | 12 ------------ .../messaging/RNFirebaseMessagingPackage.java | 12 ------------ .../firebase/perf/RNFirebasePerformancePackage.java | 12 ------------ .../firebase/storage/RNFirebaseStoragePackage.java | 12 ------------ 11 files changed, 132 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java index b960b9c6..8bf7fcbd 100644 --- a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java +++ b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java @@ -31,18 +31,6 @@ public class RNFirebasePackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobPackage.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobPackage.java index 6715affc..2bfb6109 100644 --- a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobPackage.java +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobPackage.java @@ -29,18 +29,6 @@ public class RNFirebaseAdMobPackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalyticsPackage.java b/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalyticsPackage.java index 15fe7c9e..b21a3082 100644 --- a/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalyticsPackage.java +++ b/android/src/main/java/io/invertase/firebase/analytics/RNFirebaseAnalyticsPackage.java @@ -33,18 +33,6 @@ public class RNFirebaseAnalyticsPackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuthPackage.java b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuthPackage.java index 0fa905dc..909b9312 100644 --- a/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuthPackage.java +++ b/android/src/main/java/io/invertase/firebase/auth/RNFirebaseAuthPackage.java @@ -28,18 +28,6 @@ public class RNFirebaseAuthPackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfigPackage.java b/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfigPackage.java index e794313c..dce06b80 100644 --- a/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfigPackage.java +++ b/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfigPackage.java @@ -28,18 +28,6 @@ public class RNFirebaseRemoteConfigPackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/crash/RNFirebaseCrashPackage.java b/android/src/main/java/io/invertase/firebase/crash/RNFirebaseCrashPackage.java index 8eeb6174..4ab83e78 100644 --- a/android/src/main/java/io/invertase/firebase/crash/RNFirebaseCrashPackage.java +++ b/android/src/main/java/io/invertase/firebase/crash/RNFirebaseCrashPackage.java @@ -28,18 +28,6 @@ public class RNFirebaseCrashPackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabasePackage.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabasePackage.java index 7045aa53..c4e9808d 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabasePackage.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabasePackage.java @@ -28,18 +28,6 @@ public class RNFirebaseDatabasePackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java index 528922ac..f971daea 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java @@ -28,18 +28,6 @@ public class RNFirebaseFirestorePackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java index 70ac5c94..1a88b657 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessagingPackage.java @@ -28,18 +28,6 @@ public class RNFirebaseMessagingPackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/perf/RNFirebasePerformancePackage.java b/android/src/main/java/io/invertase/firebase/perf/RNFirebasePerformancePackage.java index 04e52835..2ef2314b 100644 --- a/android/src/main/java/io/invertase/firebase/perf/RNFirebasePerformancePackage.java +++ b/android/src/main/java/io/invertase/firebase/perf/RNFirebasePerformancePackage.java @@ -28,18 +28,6 @@ public class RNFirebasePerformancePackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatability - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} diff --git a/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStoragePackage.java b/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStoragePackage.java index 114a1218..a14cc738 100644 --- a/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStoragePackage.java +++ b/android/src/main/java/io/invertase/firebase/storage/RNFirebaseStoragePackage.java @@ -33,18 +33,6 @@ public class RNFirebaseStoragePackage implements ReactPackage { return modules; } - /** - * @return list of JS modules to register with the newly created catalyst instance. - *

- * IMPORTANT: Note that only modules that needs to be accessible from the native code should be - * listed here. Also listing a native module here doesn't imply that the JS implementation of it - * will be automatically included in the JS bundle. - */ - // TODO: Removed in 0.47.0. Here for backwards compatibility - public List> createJSModules() { - return Collections.emptyList(); - } - /** * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} From fc0c57257b654015599eac3cf087b104784ec141 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 16:04:36 +0100 Subject: [PATCH 17/21] [docs] Add firestore to the keywords --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9aa3bf7b..494db69d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-native-firebase", "version": "3.0.0-alpha.5", "author": "Invertase (http://invertase.io)", - "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Auth, Database, Messaging (FCM), Remote Config, Storage, Admob, Analytics, Crash Reporting, and Performance.", + "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Auth, Database, Firestore, Messaging (FCM), Remote Config, Storage, Admob, Analytics, Crash Reporting, and Performance.", "main": "index", "scripts": { "flow": "flow", @@ -51,7 +51,8 @@ "ios", "crash", "firestack", - "performance" + "performance", + "firestore" ], "peerDependencies": { "react": "*", From 44b72f0ef60b5f51bc56ba772bd7e3244a5572e6 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 16:21:18 +0100 Subject: [PATCH 18/21] [docs] Update how version table is structured --- README.md | 18 +++++++----------- docs/README.md | 18 +++++++----------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 49839f82..a7eb17bc 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Package Quality](http://npm.packagequality.com/shield/react-native-firebase.svg?style=flat-square)](http://packagequality.com/#?package=react-native-firebase) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg?style=flat-square)](https://discord.gg/t6bdqMs) [![Donate](https://img.shields.io/badge/Donate-Patreon-green.svg?style=flat-square)](https://www.patreon.com/invertase) +[![Twitter Follow](https://img.shields.io/twitter/follow/espadrine.svg?style=social&label=Follow)](https://twitter.com/RNFirebase) **RNFirebase** makes using [Firebase](http://firebase.com) with React Native simple. @@ -69,18 +70,13 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a --- ### Supported versions - React Native / Firebase -> The table below shows the supported version of `react-native-firebase` for different React Native versions +> The table below shows the supported versions of React Native and the Firebase SDKs for different versions of `react-native-firebase` -| | v0.36 - v0.39 | v0.40 - v0.46 | v0.47 + -| ------------------------------- | :---: | :---: | :---: | -| react-native-firebase | 1.X.X | 2.X.X | 2.1.X | - -> The table below shows the minimum supported versions of the Firebase SDKs for each version of `react-native-firebase` - -| | v1 | v2 | v3 | -| ---------------------- | :---: | :---: | :---: | -| Firebase Android SDK | 10.2.0+ | 11.0.0 + | 11.2.0 + | -| Firebase iOS SDK | 3.15.0+ | 4.0.0 + | 4.0.0 + | +| | 1.X.X | 2.0.X | 2.1.X | 3.0.X | +|------------------------|-------------|-------------|----------|----------| +| React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | +| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | +| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | --- diff --git a/docs/README.md b/docs/README.md index 787f6268..d05bf0b6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ [![License](https://img.shields.io/npm/l/react-native-firebase.svg?style=flat-square)](/LICENSE) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg?style=flat-square)](https://discord.gg/t6bdqMs) [![Donate](https://img.shields.io/badge/Donate-Patreon-green.svg?style=flat-square)](https://www.patreon.com/invertase) +[![Twitter Follow](https://img.shields.io/twitter/follow/espadrine.svg?style=social&label=Follow)](https://twitter.com/RNFirebase) --- @@ -58,15 +59,10 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a --- ### Supported versions - React Native / Firebase -> The table below shows the supported version of `react-native-firebase` for different React Native versions +> The table below shows the supported versions of React Native and the Firebase SDKs for different versions of `react-native-firebase` -| | v0.36 - v0.39 | v0.40 - v0.46 | v0.47 + -| ------------------------------- | :---: | :---: | :---: | -| react-native-firebase | 1.X.X | 2.X.X | 2.1.X | - -> The table below shows the minimum supported versions of the Firebase SDKs for each version of `react-native-firebase` - -| | v1 | v2 | v3 | -| ---------------------- | :---: | :---: | :---: | -| Firebase Android SDK | 10.2.0+ | 11.0.0 + | 11.2.0 + | -| Firebase iOS SDK | 3.15.0+ | 4.0.0 + | 4.0.0 + | +| | 1.X.X | 2.0.X | 2.1.X | 3.0.X | +|------------------------|-------------|-------------|----------|----------| +| React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | +| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | +| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | From 2402acf31d92638815e60b6242d26a5e7e52f6d4 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 16:24:10 +0100 Subject: [PATCH 19/21] [docs] Update Twitter link --- README.md | 12 ++++++------ docs/README.md | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a7eb17bc..f25f08e6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Package Quality](http://npm.packagequality.com/shield/react-native-firebase.svg?style=flat-square)](http://packagequality.com/#?package=react-native-firebase) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg?style=flat-square)](https://discord.gg/t6bdqMs) [![Donate](https://img.shields.io/badge/Donate-Patreon-green.svg?style=flat-square)](https://www.patreon.com/invertase) -[![Twitter Follow](https://img.shields.io/twitter/follow/espadrine.svg?style=social&label=Follow)](https://twitter.com/RNFirebase) +[![Twitter Follow](https://img.shields.io/twitter/follow/rnfirebase.svg?style=social&label=Follow)](https://twitter.com/rnfirebase) **RNFirebase** makes using [Firebase](http://firebase.com) with React Native simple. @@ -72,11 +72,11 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a > The table below shows the supported versions of React Native and the Firebase SDKs for different versions of `react-native-firebase` -| | 1.X.X | 2.0.X | 2.1.X | 3.0.X | -|------------------------|-------------|-------------|----------|----------| -| React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | -| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | -| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | +| | 1.X.X | 2.0.X | 2.1.X / 2.2.X | 3.0.X | +|------------------------|-------------|-------------|-----------------|----------| +| React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | +| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | +| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | --- diff --git a/docs/README.md b/docs/README.md index d05bf0b6..92a0ffda 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ [![License](https://img.shields.io/npm/l/react-native-firebase.svg?style=flat-square)](/LICENSE) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg?style=flat-square)](https://discord.gg/t6bdqMs) [![Donate](https://img.shields.io/badge/Donate-Patreon-green.svg?style=flat-square)](https://www.patreon.com/invertase) -[![Twitter Follow](https://img.shields.io/twitter/follow/espadrine.svg?style=social&label=Follow)](https://twitter.com/RNFirebase) +[![Twitter Follow](https://img.shields.io/twitter/follow/rnfirebase.svg?style=social&label=Follow)](https://twitter.com/rnfirebase) --- @@ -61,8 +61,8 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a > The table below shows the supported versions of React Native and the Firebase SDKs for different versions of `react-native-firebase` -| | 1.X.X | 2.0.X | 2.1.X | 3.0.X | -|------------------------|-------------|-------------|----------|----------| -| React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | -| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | -| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | +| | 1.X.X | 2.0.X | 2.1.X / 2.2.X | 3.0.X | +|------------------------|-------------|-------------|-----------------|----------| +| React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | +| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | +| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | From 4813fa9accd8d18f3c798ab43cabf4748dcbe4ff Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 16:46:54 +0100 Subject: [PATCH 20/21] [docs] Add migration guide for v2 -> v3 --- docs/migration-guide.md | 90 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/migration-guide.md b/docs/migration-guide.md index 107f5e23..a44267db 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -2,7 +2,95 @@ ## From v2 to v3 - +The below is a quick summary of steps to take when migrating from v2 to v3 of RNFirebase. Please see the [v3 change log](https://github.com/invertase/react-native-firebase/releases/tag/v3.0.0) for detailed changes. + +** Please note, we're now using `Apache License 2.0` to license this library. ** + +##### 1) Install the latest version of RNFirebase: +> `npm i react-native-firebase@latest --save` + + + + +##### 2) Upgrade react-native version (only if you're currently lower than v0.48): + +- Follow the instructions [here](https://facebook.github.io/react-native/docs/upgrading.html) + + + + +##### 3) Update your JS code to reflect deprecations/breaking changes: + +- ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) **[breaking]** [app] `new RNFirebase()` is no longer supported. See below for information about app initialisation. +- ![#f03c15](https://placehold.it/15/fdfd96/000000?text=+) **[deprecated]** [app] `initializeApp()` for apps that are already initialised natively (i.e. the default app initialised via google-services plist/json) will now log a deprecation warning. + - As these apps are already initialised natively there's no need to call `initializeApp` in your JS code. For now, calling it will just return the app that's already internally initialised - in a future version this will throw an `already initialized` exception. + - Accessing apps can now be done the same way as the web sdk, simply call `firebase.app()` to get the default app, or with the name of specific app as the first arg, e.g. `const meow = firebase.app('catsApp');` to get a specific app. +- ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) **[breaking]** [auth] Third party providers now user `providerId` rather than `provider` as per the Web SDK. If you are manually creating your credentials, you will need to update the field name. +- ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) **[breaking]** [database] Error messages and codes internally re-written to match the web sdk +- ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) **[breaking]** [database] `ref.isEqual` now checks the query modifiers as well as the ref path (was just path before). With the release of multi apps/core support this check now also includes whether the refs are for the same app. +- ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) **[breaking]** [database] on/off behaviour changes. Previous `off` behaviour was incorrect. A `SyncTree/Repo` implementation was added to provide the correct behaviour you'd expect in the web sdk. Whilst this is a breaking change it shouldn't be much of an issue if you've previously setup your on/off handling correctly. See #160 for specifics of this change. +- ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) **[breaking]** [storage] UploadTaskSnapshot -> `downloadUrl` renamed to `downloadURL` to match web sdk + + + + + + +##### 4) Android - Update `android/build.gradle`: + + +- Check you are using google-services 3.1.0 or greater: +- You must add `maven { url 'https://maven.google.com' }` to your `android/build.gradle` as follows: +```groovy +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.google.gms:google-services:3.1.0' // CHECK VERSION HERE + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + // ADD THIS SECTION HERE + maven { + url 'https://maven.google.com' + } + } +} +``` + + + + + +##### 5) Android - Update `app/build.gradle`: + + +- You must update all your Firebase dependencies to 11.3.0. + + + + + +##### 6) iOS - Update podfile: + +- You need to check that you're running at least version 4.2.0 of the Firebase Pods + - Run `pod outdated` + - Run `pod update` ## From v1 to v2 From c558af648097c1d054f6c3afdb595fb1bf43dac4 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 3 Oct 2017 17:23:28 +0100 Subject: [PATCH 21/21] Bump to final versions of Firestore iOS and Android libraries --- README.md | 4 +- android/build.gradle | 2 +- docs/README.md | 4 +- docs/installation-ios.md | 2 +- docs/migration-guide.md | 4 +- .../firestore/RNFirebaseFirestore.h | 5 +- .../firestore/RNFirebaseFirestore.m | 19 ++-- .../RNFirebaseFirestoreCollectionReference.h | 4 +- .../RNFirebaseFirestoreCollectionReference.m | 23 +++-- .../RNFirebaseFirestoreDocumentReference.h | 4 +- .../RNFirebaseFirestoreDocumentReference.m | 12 +-- tests/android/app/build.gradle | 2 +- tests/android/app/google-services.json | 17 ++-- .../MainApplication.java | 3 +- tests/ios/GoogleService-Info.plist | 26 ++--- tests/ios/Podfile | 2 +- tests/ios/Podfile.lock | 95 ++++++++++--------- 17 files changed, 110 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index f25f08e6..e802c21a 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a | | 1.X.X | 2.0.X | 2.1.X / 2.2.X | 3.0.X | |------------------------|-------------|-------------|-----------------|----------| | React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | -| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | -| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | +| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.4.2 + | +| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.3.0 + | --- diff --git a/android/build.gradle b/android/build.gradle index 7010bfdd..121c6f3c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.firebaseVersion = '11.3.0' + ext.firebaseVersion = '11.4.2' repositories { jcenter() } diff --git a/docs/README.md b/docs/README.md index 92a0ffda..745450ce 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,5 +64,5 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a | | 1.X.X | 2.0.X | 2.1.X / 2.2.X | 3.0.X | |------------------------|-------------|-------------|-----------------|----------| | React Native | 0.36 - 0.39 | 0.40 - 0.46 | 0.47 + | 0.48 + | -| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.3.0 + | -| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.2.0 + | +| Firebase Android SDK | 10.2.0 + | 11.0.0 + | 11.0.0 + | 11.4.2 + | +| Firebase iOS SDK | 3.15.0 + | 4.0.0 + | 4.0.0 + | 4.3.0 + | diff --git a/docs/installation-ios.md b/docs/installation-ios.md index ff6b2e9b..6ee7d179 100644 --- a/docs/installation-ios.md +++ b/docs/installation-ios.md @@ -69,7 +69,7 @@ pod 'Firebase/Auth' pod 'Firebase/Crash' pod 'Firebase/Database' pod 'Firebase/DynamicLinks' -pod 'Firestore' +pod 'Firebase/Firestore' pod 'Firebase/Messaging' pod 'Firebase/RemoteConfig' pod 'Firebase/Storage' diff --git a/docs/migration-guide.md b/docs/migration-guide.md index a44267db..5a42bc87 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -80,7 +80,7 @@ allprojects { ##### 5) Android - Update `app/build.gradle`: -- You must update all your Firebase dependencies to 11.3.0. +- You must update all your Firebase dependencies to 11.4.2. @@ -88,7 +88,7 @@ allprojects { ##### 6) iOS - Update podfile: -- You need to check that you're running at least version 4.2.0 of the Firebase Pods +- You need to check that you're running at least version 4.3.0 of the Firebase Pods - Run `pod outdated` - Run `pod update` diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.h b/ios/RNFirebase/firestore/RNFirebaseFirestore.h index 4ecd5cae..e93595b1 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestore.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.h @@ -3,9 +3,9 @@ #import -#if __has_include() +#if __has_include() -#import +#import #import #import @@ -24,4 +24,3 @@ #endif #endif - diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestore.m b/ios/RNFirebase/firestore/RNFirebaseFirestore.m index 08c5c6dc..e5261c52 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestore.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestore.m @@ -1,6 +1,6 @@ #import "RNFirebaseFirestore.h" -#if __has_include() +#if __has_include() #import #import "RNFirebaseEvents.h" @@ -13,7 +13,7 @@ RCT_EXPORT_MODULE(); - (id)init { self = [super init]; if (self != nil) { - + } return self; } @@ -54,14 +54,14 @@ RCT_EXPORT_METHOD(documentBatch:(NSString *) appName rejecter:(RCTPromiseRejectBlock) reject) { FIRFirestore *firestore = [RNFirebaseFirestore getFirestoreForApp:appName]; FIRWriteBatch *batch = [firestore batch]; - + for (NSDictionary *write in writes) { NSString *type = write[@"type"]; NSString *path = write[@"path"]; NSDictionary *data = write[@"data"]; - + FIRDocumentReference *ref = [firestore documentWithPath:path]; - + if ([type isEqualToString:@"DELETE"]) { batch = [batch deleteDocument:ref]; } else if ([type isEqualToString:@"SET"]) { @@ -192,11 +192,11 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName NSMutableDictionary *errorMap = [[NSMutableDictionary alloc] init]; [errorMap setValue:@(nativeError.code) forKey:@"nativeErrorCode"]; [errorMap setValue:[nativeError localizedDescription] forKey:@"nativeErrorMessage"]; - + NSString *code; NSString *message; NSString *service = @"Firestore"; - + // TODO: Proper error codes switch (nativeError.code) { default: @@ -204,10 +204,10 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName message = [RNFirebaseFirestore getMessageWithService:@"An unknown error occurred." service:service fullCode:code]; break; } - + [errorMap setValue:code forKey:@"code"]; [errorMap setValue:message forKey:@"message"]; - + return errorMap; } @@ -221,4 +221,3 @@ RCT_EXPORT_METHOD(documentUpdate:(NSString *) appName @implementation RNFirebaseFirestore @end #endif - diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h index 9bf003a2..b03904b4 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.h @@ -2,9 +2,9 @@ #define RNFirebaseFirestoreCollectionReference_h #import -#if __has_include() +#if __has_include() -#import +#import #import #import "RNFirebaseEvents.h" #import "RNFirebaseFirestore.h" diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m index 70fb7ea6..69388409 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreCollectionReference.m @@ -2,7 +2,7 @@ @implementation RNFirebaseFirestoreCollectionReference -#if __has_include() +#if __has_include() static NSMutableDictionary *_listeners; @@ -73,7 +73,7 @@ static NSMutableDictionary *_listeners; query = [self applyFilters:query]; query = [self applyOrders:query]; query = [self applyOptions:query]; - + return query; } @@ -83,7 +83,7 @@ static NSMutableDictionary *_listeners; NSString *operator = filter[@"operator"]; // TODO: Validate this works id value = filter[@"value"]; - + if ([operator isEqualToString:@"EQUAL"]) { query = [query queryWhereField:fieldPath isEqualTo:value]; } else if ([operator isEqualToString:@"GREATER_THAN"]) { @@ -103,7 +103,7 @@ static NSMutableDictionary *_listeners; for (NSDictionary *order in _orders) { NSString *direction = order[@"direction"]; NSString *fieldPath = order[@"fieldPath"]; - + query = [query queryOrderedByField:fieldPath descending:([direction isEqualToString:@"DESCENDING"])]; } return query; @@ -138,7 +138,7 @@ static NSMutableDictionary *_listeners; [event setValue:_path forKey:@"path"]; [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"]; - + [_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; } @@ -149,7 +149,7 @@ static NSMutableDictionary *_listeners; [event setValue:_path forKey:@"path"]; [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestoreCollectionReference snapshotToDictionary:querySnapshot] forKey:@"querySnapshot"]; - + [_emitter sendEventWithName:FIRESTORE_COLLECTION_SYNC_EVENT body:event]; } @@ -157,7 +157,7 @@ static NSMutableDictionary *_listeners; NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; [snapshot setValue:[self documentChangesToArray:querySnapshot.documentChanges] forKey:@"changes"]; [snapshot setValue:[self documentSnapshotsToArray:querySnapshot.documents] forKey:@"documents"]; - + return snapshot; } @@ -166,7 +166,7 @@ static NSMutableDictionary *_listeners; for (FIRDocumentChange *change in documentChanges) { [changes addObject:[self documentChangeToDictionary:change]]; } - + return changes; } @@ -175,7 +175,7 @@ static NSMutableDictionary *_listeners; [change setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentChange.document] forKey:@"document"]; [change setValue:@(documentChange.newIndex) forKey:@"newIndex"]; [change setValue:@(documentChange.oldIndex) forKey:@"oldIndex"]; - + if (documentChange.type == FIRDocumentChangeTypeAdded) { [change setValue:@"added" forKey:@"type"]; } else if (documentChange.type == FIRDocumentChangeTypeRemoved) { @@ -183,7 +183,7 @@ static NSMutableDictionary *_listeners; } else if (documentChange.type == FIRDocumentChangeTypeModified) { [change setValue:@"modified" forKey:@"type"]; } - + return change; } @@ -192,11 +192,10 @@ static NSMutableDictionary *_listeners; for (FIRDocumentSnapshot *snapshot in documentSnapshots) { [snapshots addObject:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]]; } - + return snapshots; } #endif @end - diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h index eac466f5..51f6d257 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h @@ -3,9 +3,9 @@ #import -#if __has_include() +#if __has_include() -#import +#import #import #import "RNFirebaseEvents.h" #import "RNFirebaseFirestore.h" diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index cb56e86e..4e51a028 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -2,7 +2,7 @@ @implementation RNFirebaseFirestoreDocumentReference -#if __has_include() +#if __has_include() static NSMutableDictionary *_listeners; @@ -76,7 +76,7 @@ static NSMutableDictionary *_listeners; [self handleDocumentSnapshotEvent:listenerId documentSnapshot:snapshot]; } }; - + id listener = [_ref addSnapshotListener:listenerBlock]; _listeners[listenerId] = listener; } @@ -131,7 +131,7 @@ static NSMutableDictionary *_listeners; // createTime // readTime // updateTime - + return snapshot; } @@ -142,7 +142,7 @@ static NSMutableDictionary *_listeners; [event setValue:_path forKey:@"path"]; [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"]; - + [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } @@ -153,12 +153,10 @@ static NSMutableDictionary *_listeners; [event setValue:_path forKey:@"path"]; [event setValue:listenerId forKey:@"listenerId"]; [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"documentSnapshot"]; - + [_emitter sendEventWithName:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } #endif @end - - diff --git a/tests/android/app/build.gradle b/tests/android/app/build.gradle index ec316968..3d4eba80 100644 --- a/tests/android/app/build.gradle +++ b/tests/android/app/build.gradle @@ -71,7 +71,7 @@ android { } } -project.ext.firebaseVersion = '11.3.0' +project.ext.firebaseVersion = '11.4.2' dependencies { // compile(project(':react-native-firebase')) { diff --git a/tests/android/app/google-services.json b/tests/android/app/google-services.json index 35ae2548..30a94c5d 100644 --- a/tests/android/app/google-services.json +++ b/tests/android/app/google-services.json @@ -1,30 +1,27 @@ { "project_info": { - "project_number": "17067372085", - "firebase_url": "https://rnfirebase-5579a.firebaseio.com", - "project_id": "rnfirebase", - "storage_bucket": "rnfirebase.appspot.com" + "project_number": "305229645282", + "firebase_url": "https://rnfirebase-b9ad4.firebaseio.com", + "project_id": "rnfirebase-b9ad4", + "storage_bucket": "rnfirebase-b9ad4.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:17067372085:android:efe37851d57e1d05", + "mobilesdk_app_id": "1:305229645282:android:efe37851d57e1d05", "android_client_info": { "package_name": "com.reactnativefirebasedemo" } }, "oauth_client": [ { - "client_id": "17067372085-n572o9802h9jbv9oo60h53117pk9333k.apps.googleusercontent.com", + "client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyB-z0ytgXRRiClvslJl0tp-KbhDub9o6AM" - }, - { - "current_key": "AIzaSyAJw8mR1fPcEYC9ouZbkCStJufcCQrhmjQ" + "current_key": "AIzaSyCzbBYFyX8d6VdSu7T4s10IWYbPc-dguwM" } ], "services": { diff --git a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java index a9ec77f6..4047c71c 100644 --- a/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java +++ b/tests/android/app/src/main/java/com/reactnativefirebasedemo/MainApplication.java @@ -12,7 +12,6 @@ import io.invertase.firebase.crash.RNFirebaseCrashPackage; import io.invertase.firebase.database.RNFirebaseDatabasePackage; import io.invertase.firebase.firestore.RNFirebaseFirestorePackage; import io.invertase.firebase.messaging.RNFirebaseMessagingPackage; -import io.invertase.firebase.perf.RNFirebasePerformancePackage; import io.invertase.firebase.storage.RNFirebaseStoragePackage; import com.oblador.vectoricons.VectorIconsPackage; import com.facebook.react.ReactNativeHost; @@ -45,7 +44,7 @@ public class MainApplication extends Application implements ReactApplication { new RNFirebaseDatabasePackage(), new RNFirebaseFirestorePackage(), new RNFirebaseMessagingPackage(), - new RNFirebasePerformancePackage(), + // new RNFirebasePerformancePackage(), new RNFirebaseStoragePackage() ); } diff --git a/tests/ios/GoogleService-Info.plist b/tests/ios/GoogleService-Info.plist index 079738ad..07e6b464 100644 --- a/tests/ios/GoogleService-Info.plist +++ b/tests/ios/GoogleService-Info.plist @@ -7,34 +7,34 @@ AD_UNIT_ID_FOR_INTERSTITIAL_TEST ca-app-pub-3940256099942544/4411468910 CLIENT_ID - 17067372085-h95lq6v2fbjdl2i1f6pl26iurah37i8p.apps.googleusercontent.com + 305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.17067372085-h95lq6v2fbjdl2i1f6pl26iurah37i8p + com.googleusercontent.apps.305229645282-22imndi01abc2p6esgtu1i1m9mqrd0ib API_KEY - AIzaSyC8ZEruBCvS_6woF8_l07ILy1eXaD6J4vQ + AIzaSyAcdVLG5dRzA1ck_fa_xd4Z0cY7cga7S5A GCM_SENDER_ID - 17067372085 + 305229645282 PLIST_VERSION 1 BUNDLE_ID com.invertase.ReactNativeFirebaseDemo PROJECT_ID - rnfirebase + rnfirebase-b9ad4 STORAGE_BUCKET - rnfirebase.appspot.com + rnfirebase-b9ad4.appspot.com IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:17067372085:ios:7b45748cb1117d2d + 1:305229645282:ios:7b45748cb1117d2d DATABASE_URL - https://rnfirebase-5579a.firebaseio.com + https://rnfirebase-b9ad4.firebaseio.com diff --git a/tests/ios/Podfile b/tests/ios/Podfile index 6b0e7fbd..d471bdd1 100644 --- a/tests/ios/Podfile +++ b/tests/ios/Podfile @@ -25,7 +25,7 @@ target 'ReactNativeFirebaseDemo' do pod 'Firebase/Crash' pod 'Firebase/Database' pod 'Firebase/DynamicLinks' - pod 'Firestore', :podspec => 'https://storage.googleapis.com/firebase-preview-drop/ios/firestore/0.7.0/Firestore.podspec.json' + pod 'Firebase/Firestore' pod 'Firebase/Messaging' pod 'Firebase/RemoteConfig' pod 'Firebase/Storage' diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index d1aad066..5ea77fb7 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -5,46 +5,49 @@ PODS: - BoringSSL/Implementation (9.0): - BoringSSL/Interface (= 9.0) - BoringSSL/Interface (9.0) - - Firebase/AdMob (4.2.0): + - Firebase/AdMob (4.3.0): - Firebase/Core - - Google-Mobile-Ads-SDK (= 7.24.0) - - Firebase/Auth (4.2.0): + - Google-Mobile-Ads-SDK (= 7.24.1) + - Firebase/Auth (4.3.0): - Firebase/Core - - FirebaseAuth (= 4.2.0) - - Firebase/Core (4.2.0): - - FirebaseAnalytics (= 4.0.3) - - FirebaseCore (= 4.0.7) - - Firebase/Crash (4.2.0): + - FirebaseAuth (= 4.2.1) + - Firebase/Core (4.3.0): + - FirebaseAnalytics (= 4.0.4) + - FirebaseCore (= 4.0.8) + - Firebase/Crash (4.3.0): - Firebase/Core - FirebaseCrash (= 2.0.2) - - Firebase/Database (4.2.0): + - Firebase/Database (4.3.0): - Firebase/Core - - FirebaseDatabase (= 4.0.3) - - Firebase/DynamicLinks (4.2.0): + - FirebaseDatabase (= 4.1.0) + - Firebase/DynamicLinks (4.3.0): - Firebase/Core - FirebaseDynamicLinks (= 2.1.0) - - Firebase/Messaging (4.2.0): + - Firebase/Firestore (4.3.0): - Firebase/Core - - FirebaseMessaging (= 2.0.3) - - Firebase/Performance (4.2.0): + - FirebaseFirestore (= 0.8.0) + - Firebase/Messaging (4.3.0): - Firebase/Core - - FirebasePerformance (= 1.0.5) - - Firebase/RemoteConfig (4.2.0): + - FirebaseMessaging (= 2.0.4) + - Firebase/Performance (4.3.0): + - Firebase/Core + - FirebasePerformance (= 1.0.6) + - Firebase/RemoteConfig (4.3.0): - Firebase/Core - FirebaseRemoteConfig (= 2.0.3) - - Firebase/Storage (4.2.0): + - Firebase/Storage (4.3.0): - Firebase/Core - FirebaseStorage (= 2.0.2) - - FirebaseAnalytics (4.0.3): + - FirebaseAnalytics (4.0.4): - FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/NSData+zlib (~> 2.1) - nanopb (~> 0.3) - - FirebaseAuth (4.2.0): + - FirebaseAuth (4.2.1): - FirebaseAnalytics (~> 4.0) - GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1) - GTMSessionFetcher/Core (~> 1.1) - - FirebaseCore (4.0.7): + - FirebaseCore (4.0.8): - GoogleToolboxForMac/NSData+zlib (~> 2.1) - nanopb (~> 0.3) - FirebaseCrash (2.0.2): @@ -53,20 +56,27 @@ PODS: - GoogleToolboxForMac/Logger (~> 2.1) - GoogleToolboxForMac/NSData+zlib (~> 2.1) - Protobuf (~> 3.1) - - FirebaseDatabase (4.0.3): + - FirebaseDatabase (4.1.0): - FirebaseAnalytics (~> 4.0) - FirebaseCore (~> 4.0) - leveldb-library (~> 1.18) - FirebaseDynamicLinks (2.1.0): - FirebaseAnalytics (~> 4.0) - - FirebaseInstanceID (2.0.3) - - FirebaseMessaging (2.0.3): + - FirebaseFirestore (0.8.0): + - FirebaseAnalytics (~> 4.0) + - FirebaseAuth (~> 4.2) + - FirebaseCore (~> 4.0) + - gRPC-ProtoRPC (~> 1.0) + - leveldb-library (~> 1.18) + - Protobuf (~> 3.1) + - FirebaseInstanceID (2.0.4) + - FirebaseMessaging (2.0.4): - FirebaseAnalytics (~> 4.0) - FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/Logger (~> 2.1) - Protobuf (~> 3.1) - - FirebasePerformance (1.0.5): + - FirebasePerformance (1.0.6): - FirebaseAnalytics (~> 4.0) - FirebaseInstanceID (~> 2.0) - GoogleToolboxForMac/Logger (~> 2.1) @@ -82,14 +92,7 @@ PODS: - FirebaseAnalytics (~> 4.0) - FirebaseCore (~> 4.0) - GTMSessionFetcher/Core (~> 1.1) - - Firestore (0.7.0): - - FirebaseAnalytics (~> 4.0) - - FirebaseAuth (~> 4.1) - - FirebaseCore (~> 4.0) - - gRPC-ProtoRPC (~> 1.0) - - leveldb-library (~> 1.18) - - Protobuf (~> 3.1) - - Google-Mobile-Ads-SDK (7.24.0) + - Google-Mobile-Ads-SDK (7.24.1) - GoogleToolboxForMac/DebugUtils (2.1.1): - GoogleToolboxForMac/Defines (= 2.1.1) - GoogleToolboxForMac/Defines (2.1.1) @@ -158,11 +161,11 @@ DEPENDENCIES: - Firebase/Crash - Firebase/Database - Firebase/DynamicLinks + - Firebase/Firestore - Firebase/Messaging - Firebase/Performance - Firebase/RemoteConfig - Firebase/Storage - - Firestore (from `https://storage.googleapis.com/firebase-preview-drop/ios/firestore/0.7.0/Firestore.podspec.json`) - React/BatchedBridge (from `../node_modules/react-native`) - React/Core (from `../node_modules/react-native`) - React/RCTNetwork (from `../node_modules/react-native`) @@ -172,8 +175,6 @@ DEPENDENCIES: - yoga (from `../node_modules/react-native/ReactCommon/yoga`) EXTERNAL SOURCES: - Firestore: - :podspec: https://storage.googleapis.com/firebase-preview-drop/ios/firestore/0.7.0/Firestore.podspec.json React: :path: "../node_modules/react-native" RNFirebase: @@ -183,20 +184,20 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BoringSSL: 19083b821ef3ae0f758fae15482e183003b1e265 - Firebase: 9548cae14d69718add12d75a5b312893f7ef89c7 - FirebaseAnalytics: 76f754d37ca5b04f36856729b6af3ca0152d1069 - FirebaseAuth: 22f8a5170f31d1f111141950590f071f35df3229 - FirebaseCore: 9a6cc1e3eaf75905390f9220596ad4fd8f92faee + Firebase: 83283761a1ef6dc9846e03d08059f51421afbd65 + FirebaseAnalytics: 722b53c7b32bfc7806b06e0093a2f5180d4f2c5a + FirebaseAuth: d7f047fbeab98062b98ea933b8d934e0fb1190e2 + FirebaseCore: 69b1a5ac5f857ba6d5fd9d5fe794f4786dd5e579 FirebaseCrash: cded0fc566c03651aea606a101bc156085f333ca - FirebaseDatabase: '03940adcac54ce30db06f1fc2136f8581734ce2c' + FirebaseDatabase: 607284a103e961d7f5863ee603cab5e85f443bd6 FirebaseDynamicLinks: ed4cb6c42705aaa5e841ed2d76e3a4bddbec10c1 - FirebaseInstanceID: a4fc702b5a026f7322964376047f1a3f1f7cc6ff - FirebaseMessaging: eaf1bfff0193170c04ea3ba3bfe983f68f893118 - FirebasePerformance: d0dc2a1d3dc1bca249d154cb40ee4eae25b455ad + FirebaseFirestore: 8e2fd99a621ae6fc6acfac3bdea824fe9d9c128d + FirebaseInstanceID: 70c2b877e9338971b2429ea5a4293df6961aa44e + FirebaseMessaging: 3dd86bfda2acb680b05c97f3f8ac566e9bb87b2a + FirebasePerformance: fa032c27e229eb8c1a8638918793fe2e47465205 FirebaseRemoteConfig: 1c982f73af48ec048c8fa8621d5178cfdffac9aa FirebaseStorage: 0cca42d9b889a0227c3a50121f45a4469fc9eb27 - Firestore: 5a33dcb27d8d33d4e82e032ee31ecd2904a72167 - Google-Mobile-Ads-SDK: f405b7acb098fe89e6fcd05fdbf400c1a5bcb935 + Google-Mobile-Ads-SDK: ed8004a7265b424568dc84f3d2bbe3ea3fff958f GoogleToolboxForMac: 8e329f1b599f2512c6b10676d45736bcc2cbbeb0 gRPC: '07788969b862af21491908f82b83d17ac08c94cd' gRPC-Core: f707ade59c559fe718e27713189607d03b15f571 @@ -207,9 +208,9 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 03eef2ee0b674770735cf79d9c4d3659cf6908e8 React: e6ef6a41ec6dd1b7941417d60ca582bf5e9c739d - RNFirebase: 60be8c01b94551a12e7be5431189e8ee8cefcdd3 + RNFirebase: 6508ffd6cab78cc3a84305708a250d7d4b74f2dc yoga: f9485d2ebf0ca773db2d727ea71b1aa8c9f3e075 -PODFILE CHECKSUM: 486420a7b0a6a9fb2869e71a7f85e245f2aab1b2 +PODFILE CHECKSUM: b5674be55653f5dda937c8b794d0479900643d45 COCOAPODS: 1.2.1