typescriptfirebasegoogle-cloud-firestorereflection

FireStore - Converter for nested cusom objects


I would like to create a Firebase converter for nested objects. I based my code on the Firebase tutorial

Nested objects

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

Adapted converter

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

Universal converter

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 ?


Solution

  • Simple generic converter

    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.

    Complete generic converter

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