Skip to content

Instantly share code, notes, and snippets.

@edujjalvarez
Last active September 11, 2022 14:46
Show Gist options
  • Save edujjalvarez/b11791f46d3c865bc15120652cf72b42 to your computer and use it in GitHub Desktop.
Save edujjalvarez/b11791f46d3c865bc15120652cf72b42 to your computer and use it in GitHub Desktop.
Base typescript service to CRUD (GetAll, GetById, Add, Update, SoftDelete and HardDelete) using Firestore in React Native
import { BaseModel } from "../models/base.model";
import { Order } from "../models/order";
import firestore, {
FirebaseFirestoreTypes,
} from "@react-native-firebase/firestore";
import { Page } from "../models/page";
import uuid from "react-native-uuid";
export class BaseModel {
id?: string;
searchTerms?: string[];
insertedAt?: number;
insertedBy?: string | null;
updatedAt?: number | null;
updatedBy?: string | null;
deletedAt?: number | null;
deletedBy?: string | null;
deleted?: boolean;
constructor() {
this.id = uuid.v4() as string;
this.searchTerms = [];
this.insertedAt = Date.now();
this.insertedBy = null;
this.updatedAt = null;
this.updatedBy = null;
this.deletedAt = null;
this.deletedBy = null;
this.deleted = false;
}
public static setValuesOnAdd(model: BaseModel, insertedBy?: string): void {
if (!model) {
return;
}
const now = Date.now();
model.insertedAt = now;
model.insertedBy = insertedBy ? insertedBy : null;
model.updatedAt = now;
model.updatedBy = insertedBy ? insertedBy : null;
model.deletedAt = null;
model.deletedBy = null;
model.deleted = false;
}
public static setValuesOnUpdate(model: BaseModel, updatedBy?: string): void {
if (!model) {
return;
}
model.updatedAt = Date.now();
model.updatedBy = updatedBy ? updatedBy : null;
}
public static setValuesOnDelete(model: BaseModel, deletedBy?: string): void {
if (!model) {
return;
}
model.deletedAt = Date.now();
model.deletedBy = deletedBy ? deletedBy : null;
model.deleted = true;
}
public static copyValues<T>(from: any, to: any): T {
if (!from) {
return to;
}
for (const key in from) {
if (key in from && key in to) {
const value = from[key];
to[key] = value;
}
}
return to;
}
}
export enum Direction {
asc = 'asc',
desc = 'desc',
}
export interface Order {
by: string;
dir: Direction;
}
export interface Filter {
by: string;
operator: FirebaseFirestoreTypes.WhereFilterOp;
value: any;
}
export interface Page<T> {
limit: number;
filters?: Filter[];
order?: Order;
results?: T[];
lastDocumentData?: FirebaseFirestoreTypes.DocumentData | undefined;
}
export class BaseService<T extends BaseModel> {
TAG = BaseService.name;
public readonly Firestore: FirebaseFirestoreTypes.Module;
private readonly Path: string;
constructor(path: string) {
this.Firestore = firestore();
this.Path = path;
}
public static createId(): string {
const CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let autoId = "";
for (let i = 0; i < 20; i++) {
autoId += CHARS.charAt(Math.floor(Math.random() * CHARS.length));
}
return autoId;
}
async getAllAsync(
order?: Order,
includeDeleted?: boolean,
callback?: any
): Promise<T[]> {
console.log(`${this.TAG} > getAllAsync`);
if (!includeDeleted) {
includeDeleted = false;
}
const promise: Promise<T[]> = new Promise(async (resolve, reject) => {
try {
let querySnapshot: FirebaseFirestoreTypes.QuerySnapshot<FirebaseFirestoreTypes.DocumentData>;
if (order) {
querySnapshot = await this.Firestore.collection(`${this.Path}`)
.orderBy(order.by, order.dir)
.get();
} else {
querySnapshot = await this.Firestore.collection(`${this.Path}`).get();
}
const fromCache = querySnapshot.metadata.fromCache;
console.log(`${this.TAG}> getAllAsync > fromCache = ${fromCache}`);
const entities: T[] = [];
querySnapshot.forEach((queryDocumentSnapshot) => {
const entity = queryDocumentSnapshot.data() as T;
console.log(`${this.TAG}> getAllAsync > entity`, entity);
if (
includeDeleted === true ||
entity.deleted === false ||
!entity["deleted"]
) {
entities.push(entity);
}
});
console.log(`${this.TAG}> getAllAsync > entities`, entities);
if (callback) {
callback(entities);
}
resolve(entities);
} catch (error) {
console.error(`${this.TAG} > getAllAsync > error`);
reject(error);
}
});
return promise;
}
async getPageAsync(
page: Page<T>,
includeDeleted?: boolean,
callback?: any
): Promise<Page<T>> {
console.log(`${this.TAG} > getPageAsync`);
if (!includeDeleted) {
includeDeleted = false;
}
const promise: Promise<Page<T>> = new Promise(async (resolve, reject) => {
try {
if (!page || !page.limit) reject("page and page.limit are required");
const collection = this.Firestore.collection(`${this.Path}`);
let query:
| FirebaseFirestoreTypes.Query<FirebaseFirestoreTypes.DocumentData>
| undefined;
if (page.filters && page.filters.length > 0) {
console.log(`${this.TAG} > getPageAsync > set page.filters query`);
page.filters.forEach((f) => {
if (
!f.by ||
!f.operator ||
f.value == null ||
f.value == undefined ||
(Array.isArray(f.value) && f.value.length == 0)
)
return;
query = query
? query.where(f.by, f.operator, f.value)
: collection.where(f.by, f.operator, f.value);
});
}
if (page.order) {
console.log(`${this.TAG} > getPageAsync > set page.order query`);
query = query
? query.orderBy(page.order.by, page.order.dir)
: collection.orderBy(page.order.by, page.order.dir);
}
if (page.lastDocumentData) {
console.log(
`${this.TAG} > getPageAsync > set page.lastDocumentData query`
);
query = query
? query.startAfter(page.lastDocumentData)
: collection.startAfter(page.lastDocumentData);
}
let querySnapshot: FirebaseFirestoreTypes.QuerySnapshot<FirebaseFirestoreTypes.DocumentData> =
query
? await query.limit(page.limit).get()
: await collection.limit(page.limit).get();
const fromCache = querySnapshot.metadata.fromCache;
console.log(`${this.TAG} > getPageAsync > fromCache = ${fromCache}`);
const entities: T[] = [];
querySnapshot.forEach((queryDocumentSnapshot, i) => {
const documentData = queryDocumentSnapshot.data();
const entity = documentData as T;
// console.log(`${this.TAG}> getPageAsync > entity`, entity);
if (
includeDeleted === true ||
entity.deleted === false ||
!entity["deleted"]
) {
entities.push(entity);
}
// if (i == querySnapshot.size - 1) {
// page.lastDocumentData = documentData;
// }
});
page.lastDocumentData =
querySnapshot.docs[querySnapshot.docs.length - 1];
page.results = entities;
// console.log(`${this.TAG}> getPageAsync > page`, page);
if (callback) {
callback(page);
}
resolve(page);
} catch (error) {
console.error(`${this.TAG} > getPageAsync > error`);
reject(error);
}
});
return promise;
}
async getByIdAsync(id: string, callback?: any): Promise<T | null> {
console.log(`${this.TAG} > getByIdAsync > id`, id);
const promise: Promise<T | null> = new Promise(async (resolve, reject) => {
if (!id) {
reject("id is required");
}
try {
const documentSnapshot = await this.Firestore.collection(`${this.Path}`)
.doc(`${id}`)
.get();
const fromCache = documentSnapshot.metadata.fromCache;
console.log(`${this.TAG}> getByIdAsync > fromCache = ${fromCache}`);
const entity: T | null = documentSnapshot.exists
? (documentSnapshot.data() as T)
: null;
if (callback) {
callback(entity);
}
resolve(entity);
} catch (error) {
console.error(`${this.TAG} > getByIdAsync > error`, error);
reject(error);
}
});
return promise;
}
async addAsync(entity: T, callback?: any): Promise<T | null> {
console.log(`${this.TAG} > addAsync > entity`, entity);
const promise: Promise<T | null> = new Promise(async (resolve, reject) => {
if (!entity) {
reject("Entity is required");
}
try {
BaseModel.setValuesOnAdd(entity);
if (!entity.id) {
entity.id = BaseService.createId();
}
await this.Firestore.collection(`${this.Path}`)
.doc(entity.id)
.set(entity);
if (callback) {
callback(entity);
}
resolve(entity);
} catch (error) {
console.error(`${this.TAG} > addAsync > error`, error);
reject(error);
}
});
return promise;
}
async updateAsync(entity: T, callback?: any): Promise<T | null> {
console.log(`${this.TAG} > updateAsync > entity`, entity);
const promise: Promise<T | null> = new Promise(async (resolve, reject) => {
if (!entity || !entity.id) {
reject("entity is required");
}
try {
BaseModel.setValuesOnUpdate(entity);
await this.Firestore.collection(`${this.Path}`)
.doc(`${entity.id}`)
.update(entity);
if (callback) {
callback(entity);
}
resolve(entity);
} catch (error) {
console.error(`${this.TAG} > updateAsync > error`, error);
reject(error);
}
});
return promise;
}
async softDeleteAsync(entity: T, callback?: any): Promise<T | null> {
console.log(`${this.TAG} > softDeleteAsync > entity`, entity);
const promise: Promise<T | null> = new Promise(async (resolve, reject) => {
if (!entity || !entity.id) {
reject("entity is required");
}
try {
BaseModel.setValuesOnDelete(entity);
await this.Firestore.collection(`${this.Path}`)
.doc(`${entity.id}`)
.update(entity);
if (callback) {
callback(entity);
}
resolve(entity);
} catch (error) {
console.error(`${this.TAG} > softDeleteAsync > error`, error);
reject(error);
}
});
return promise;
}
async hardDeleteAsync(entity: T, callback?: any): Promise<void> {
console.log(`${this.TAG} > hardDeleteAsync > entity`, entity);
const promise: Promise<void> = new Promise(async (resolve, reject) => {
if (!entity || !entity.id) {
reject("entity is required");
}
try {
BaseModel.setValuesOnDelete(entity);
await this.Firestore.collection(`${this.Path}`)
.doc(`${entity.id}`)
.delete();
if (callback) {
callback();
}
resolve();
} catch (error) {
console.error(`${this.TAG} > hardDeleteAsync > error`, error);
reject(error);
}
});
return promise;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment