Last active
September 11, 2022 14:46
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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