Skip to content
This repository has been archived by the owner on Sep 5, 2023. It is now read-only.

[WIP] Add @mobilejazz/harmony-firebase #85

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/firebase/data/firestore-paginated.data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import firebase from 'firebase/app';
import { GetDataSource, PaginationPage, Query, QueryNotSupportedError } from '@mobilejazz/harmony-core';

import { FirestoreEntity } from './firestore.entity';
import { FirestorePaginationNextQuery, FirestorePaginationPreviousQuery } from './firestore.query';

export class FirestorePage<T> extends PaginationPage<T> {
constructor(
values: T[],
page: number,
size: number,
readonly firstDoc: firebase.firestore.DocumentSnapshot,
readonly lastDoc: firebase.firestore.DocumentSnapshot,
) {
super(values, page, size);
}
}

export class FirestorePaginatedDataSource<T> implements GetDataSource<FirestorePage<FirestoreEntity<T>>> {
protected collection: firebase.firestore.CollectionReference;

constructor(protected collectionName: string, protected firestore: firebase.firestore.Firestore) {
this.collection = this.firestore.collection(collectionName);
}

public async get(query: Query): Promise<FirestorePage<FirestoreEntity<T>>> {
if (query instanceof FirestorePaginationNextQuery || query instanceof FirestorePaginationPreviousQuery) {
let fsQuery: firebase.firestore.Query<firebase.firestore.DocumentData>;

// Check if `doc<id>` filter is present
const idFilter = (query.filterBy || []).find(f => f.fieldPath === 'doc<id>');
const hasIdFilter = idFilter !== undefined;

if (hasIdFilter) {
const doc = await this.collection.doc(idFilter.value).get();
const entity = { id: doc.id, data: doc.data() as T };

if (doc.exists) {
return new FirestorePage([entity], query.page, query.size, doc, doc);
} else {
return new FirestorePage([], query.page, query.size, undefined, undefined);
}
}

// Order
if (query.orderBy) {
const filters = query.filterBy ?? [];
const isOrderInFilter = filters.some(f => f.fieldPath === query.orderBy.field);

// If inequality filters are applied (e.g. for searching on a field that starts by a word) then
// the first `orderBy`, if present, must be on the same field that it's being filtered. Which makes `orderBy` useless.
// That's why if there is an inequality filter we ignore ordering. Firebase rulez. -_-
const hasInequalityFilter = filters.some(f => ['<', '<=', '>', '>='].includes(f.opStr));

// Apply order only if we're not filtering on the same field
if (!isOrderInFilter && !hasInequalityFilter) {
fsQuery = this.collection.orderBy(query.orderBy.field, query.orderBy.direction);
}
}

if (query instanceof FirestorePaginationNextQuery) {
fsQuery = (fsQuery ? fsQuery : this.collection).limit(query.size);

(query.filterBy || []).forEach(f => {
fsQuery = fsQuery.where(f.fieldPath, f.opStr, f.value);
});

if (query.lastDoc) {
fsQuery = fsQuery.startAfter(query.lastDoc);
}
} else if (query instanceof FirestorePaginationPreviousQuery) {
fsQuery = (fsQuery ? fsQuery : this.collection).limitToLast(query.size);

(query.filterBy || []).forEach(f => {
fsQuery = fsQuery.where(f.fieldPath, f.opStr, f.value);
});

if (query.firstDoc) {
fsQuery = fsQuery.endBefore(query.firstDoc);
}
}

// Get
const entries = await fsQuery.get();
const values = entries.docs.map((doc: firebase.firestore.QueryDocumentSnapshot) => {
return { id: doc.id, data: doc.data() as T };
});

if (values.length) {
return new FirestorePage(
values,
query.page,
query.size,
entries.docs[0],
entries.docs[entries.docs.length - 1],
);
} else {
return new FirestorePage(values, query.page, query.size, undefined, undefined);
}
}

throw new QueryNotSupportedError();
}

public getAll(query: Query): Promise<FirestorePage<FirestoreEntity<T>>[]> {
throw new QueryNotSupportedError();
}
}
117 changes: 117 additions & 0 deletions packages/firebase/data/firestore.data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import firebase from 'firebase/app';
import {
DeleteDataSource,
GetDataSource,
IdQuery,
NotFoundError,
PutDataSource,
Query,
QueryNotSupportedError,
} from '@mobilejazz/harmony-core';

import { FirestoreIncrementQuery, FirestoreCreateDocumentQuery } from './firestore.query';
import { FirestoreEntity } from './firestore.entity';

export class FirestoreDataSource<T>
implements GetDataSource<FirestoreEntity<T>>, PutDataSource<FirestoreEntity<T>>, DeleteDataSource
{
protected collection: firebase.firestore.CollectionReference;

constructor(protected collectionName: string, protected firestore: firebase.firestore.Firestore) {
this.collection = this.firestore.collection(collectionName);
}

protected async findOne(query: firebase.firestore.Query): Promise<FirestoreEntity<T>> {
const result = await query.limit(1).get();

if (!result.empty) {
const doc = result.docs[0];

return {
id: doc.id,
data: doc.data() as T,
};
} else {
throw new NotFoundError();
}
}

async get(query: Query): Promise<FirestoreEntity<T>> {
if (query instanceof IdQuery) {
const doc = await this.collection.doc(query.id).get();

if (doc.exists) {
return {
id: doc.id,
data: doc.data() as T,
};
} else {
throw new NotFoundError();
}
}

throw new QueryNotSupportedError();
}

getAll(query: Query): Promise<FirestoreEntity<T>[]> {
throw new QueryNotSupportedError();
}

delete(query: Query): Promise<void> {
throw new QueryNotSupportedError();
}

deleteAll(query: Query): Promise<void> {
throw new QueryNotSupportedError();
}

async put(value: FirestoreEntity<T>, query: Query): Promise<FirestoreEntity<T>> {
if (query instanceof IdQuery) {
const ref = this.collection.doc(query.id);
const doc = await ref.get();

if (doc.exists) {
await ref.update(value.data);
} else {
await ref.set(value.data);
}

return {
id: doc.id,
data: doc.data() as T,
};
} else if (query instanceof FirestoreCreateDocumentQuery) {
if (value.id) {
const ref = this.collection.doc(value.id);
await ref.set(value.data);

return value;
} else {
const ref = await this.collection.add(value.data);
const doc = await ref.get();

return {
id: doc.id,
data: doc.data() as T,
};
}
} else if (query instanceof FirestoreIncrementQuery) {
const increment = firebase.firestore.FieldValue.increment(query.amount);
const userRef = this.collection.doc(value.id);
const data = {};

// Build udpate "instructions"
data[query.field] = increment;

await userRef.update(data);

return value;
}

throw new QueryNotSupportedError();
}

putAll(values: FirestoreEntity<T>[], query: Query): Promise<FirestoreEntity<T>[]> {
throw new QueryNotSupportedError();
}
}
4 changes: 4 additions & 0 deletions packages/firebase/data/firestore.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface FirestoreEntity<T> {
id: string;
data: T;
}
49 changes: 49 additions & 0 deletions packages/firebase/data/firestore.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import firebase from 'firebase/app';
import { PaginationQuery, Query } from '@mobilejazz/harmony-core';

export class FirestoreCreateDocumentQuery extends Query {}

export class FirestoreIncrementQuery extends Query {
constructor(readonly field: string, readonly amount: number) {
super();
}
}

export class FirestoreWhereClause {
constructor(
readonly fieldPath: string | firebase.firestore.FieldPath,
readonly opStr: firebase.firestore.WhereFilterOp,
readonly value: any,
) {}
}

export type FirestoreFilter = FirestoreWhereClause[];

export interface FirestoreOrder {
field: string;
direction?: 'asc' | 'desc';
}

export class FirestorePaginationNextQuery extends PaginationQuery {
constructor(
readonly page: number,
readonly size: number,
readonly filterBy: FirestoreFilter,
readonly orderBy: FirestoreOrder,
readonly lastDoc: firebase.firestore.DocumentSnapshot,
) {
super();
}
}

export class FirestorePaginationPreviousQuery extends PaginationQuery {
constructor(
readonly page: number,
readonly size: number,
readonly filterBy: FirestoreFilter,
readonly orderBy: FirestoreOrder,
readonly firstDoc: firebase.firestore.DocumentSnapshot,
) {
super();
}
}
4 changes: 4 additions & 0 deletions packages/firebase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './data/firestore-paginated.data-source';
export * from './data/firestore.data-source';
export * from './data/firestore.entity';
export * from './data/firestore.query';