I would like to create a Firebase converter for nested objects. I based my code on the Firebase tutorial
Here is an object similar to the one in the tuto, but with a nested class:
class City {
name?: string;
state?: string;
country?: Country;
}
class Country {
name?: string;
language?: string;
}
Here the converter completed from the one in tutorial :
const cityConverter = {
toFirestore: (city: any) => {
return {
name: city.name,
state: city.state,
country: {
name: city.country.name,
language: city.country.language
}
};
},
fromFirestore: (snapshot: any, options: any) => {
const data = snapshot.data(options);
const country = new Country();
country.name = data.name;
country.language = data.language;
const city = new City();
city.name = data.name;
city.state = data.state;
city.country = country;
return city;
}
};
It works but fails when a value is undefined
As it is time consuming to write all fields, here all fieds are listed with reflection.
It is used with new FirestoreConverter<City>()
class FirestoreConverter<T extends object> {
toFirestore(dto: T): DocumentData {
const data: DocumentData = {};
Object.keys(dto as object).forEach((key) => {
data[key] = Reflect.get(dto, key);
});
return data
}
fromFirestore(snapshot: QueryDocumentSnapshot<T>): T {
const data = snapshot.data();
const dto = {} as T;
Object.keys(data as object).forEach((key) => {
Reflect.set(dto, key, data[key as keyof T]);
});
return dto;
}
}
It works, but not on nested objects
Would it be possible to improve it to support nested custom objects ?
class FirestoreConverter<T extends object> {
readonly IGNORE_UNDEFINED: boolean = true
toFirestore(dto: T): DocumentData {
return this.toFirestoreRecurse(dto)
}
toFirestoreRecurse(dto: any): DocumentData {
const data: DocumentData = {};
Object.keys(dto).forEach((key) => {
const value = Reflect.get(dto, key);
if (value === undefined) {
if (!this.IGNORE_UNDEFINED) data[key] = null
}
else if (Array.isArray(value))
data[key] = value.map(v => Object(value) === value ? this.toFirestoreRecurse(v) : value)
else if (Object(value) === value)
data[key] = this.toFirestoreRecurse(value)
else
data[key] = value
});
return data
}
fromFirestore(snapshot: QueryDocumentSnapshot<any>): T {
const data = snapshot.data();
const dto: T = {} as T;
Object.keys(data as object).forEach((key) => {
Reflect.set(dto, key, data[key as keyof T]);
});
return dto;
}
}
It works but fails on nested arrays (as they are not supported in Firestore. Following is a workaround to rebuild nested array.
class FirestoreConverter<T extends object> {
readonly IGNORE_UNDEFINED: boolean = true
readonly type;
constructor(type: { new(): T; }) {
this.type = type
}
toFirestore(dto: T): DocumentData {
return this.toFirestoreRecurse(dto)
}
toFirestoreRecurse(dto: any): DocumentData {
const data: DocumentData = {};
Object.keys(dto).forEach((key) => {
const value = Reflect.get(dto, key);
if (value === undefined) {
if (!this.IGNORE_UNDEFINED) data[key] = null
}
else if (Array.isArray(value))
data[key] = value.map(v => Object(v) === v ? this.toFirestoreRecurse(v) : v)
else if (Object(value) === value)
data[key] = this.toFirestoreRecurse(value)
else
data[key] = value
});
return data
}
fromFirestore(snapshot: QueryDocumentSnapshot<any>): T {
const data = snapshot.data();
const dto = new this.type()
return this.fromFirestoreRecurse(data, dto) as T;
}
fromFirestoreRecurse(data: any): any {
if (dto === undefined)
dto = {}
Object.keys(data as object).forEach((key) => {
let value = data[key]
if (Array.isArray(value))
value = this.mapArrayRecurse(value)
else if (Object(value) === value)
value = this.fromFirestoreRecurse(value)
Reflect.set(dto, key, value)
});
return dto;
}
mapArrayRecurse(array: any[]): any[] {
// Is empty or primitive
if (array.length === 0 || Object(array[0]) !== array[0])
return array
// Is poorly Converted Array
if (Object.keys(array[0]).every((x, index) => Number(x) === index))
return array.map(x => this.mapArrayRecurse(Object.values(x)))
// Is Object
return array.map(x => this.fromFirestoreRecurse(x))
}
}