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; +}