[firestore][js] First pass of javascript implementation

This commit is contained in:
Chris Bianca 2017-09-26 14:57:25 +01:00
parent 261edf7a83
commit dfd9080281
11 changed files with 946 additions and 0 deletions

View File

@ -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<DocumentReference> {
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<QuerySnapshot> {
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<DocumentSnapshot> {
return this._query.stream();
}
where(fieldPath: string, opStr: Operator, value: any): Query {
return this._query.where(fieldPath, opStr, value);
}
}

View File

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

View File

@ -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<WriteResult> {
return this._firestore._native
.documentCreate(this._documentPath._parts, data);
}
delete(deleteOptions?: DeleteOptions): Promise<WriteResult> {
return this._firestore._native
.documentDelete(this._documentPath._parts, deleteOptions);
}
get(): Promise<DocumentSnapshot> {
return this._firestore._native
.documentGet(this._documentPath._parts)
.then(result => new DocumentSnapshot(this._firestore, result));
}
getCollections(): Promise<CollectionReference[]> {
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<WriteResult> {
return this._firestore._native
.documentSet(this._documentPath._parts, data, writeOptions);
}
update(data: { [string]: any }, updateOptions?: UpdateOptions): Promise<WriteResult> {
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('/')}`;
}
}

View File

@ -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];
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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<QuerySnapshot> {
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<DocumentSnapshot> {
}
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);
}
}

View File

@ -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);
}
}
}

View File

@ -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<WriteResult[]> {
return this._firestore._native
.documentBatch(this._writes, commitOptions);
}
}

View File

@ -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<DocumentSnapshot[]> {
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<CollectionReference[]> {
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 || {}
},
};

View File

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