import { DataSource , DeleteEntityProps , Entity , EntityReference , EntitySchema , EntitySchemaResolver , EntityValues , FetchCollectionProps , FetchEntityProps , FilterValues , GeoPoint , ListenCollectionProps , ListenEntityProps , Properties , Property , ResolvedEntitySchema , SaveEntityProps , WhereFilterOp } from "../../models" ; import { computeSchema , sanitizeData , traverseValues , updateAutoValues } from "../../core/utils" ; import { collection , CollectionReference , deleteDoc , doc , DocumentReference , DocumentSnapshot , Firestore , GeoPoint as FirestoreGeoPoint , getDoc , getDocs , getFirestore , limit as limitClause , onSnapshot , orderBy as orderByClause , Query , query , serverTimestamp , setDoc , startAfter as startAfterClause , Timestamp , where as whereClause } from "firebase/firestore" ; import { FirebaseApp } from "firebase/app" ; import { FirestoreTextSearchController } from "../models/text_search" ; import { useEffect , useRef } from "react" ; category export interface FirestoreDataSourceProps { firebaseApp ? : FirebaseApp ; textSearchController ? : FirestoreTextSearchController ; } /**
 * Use this hook to build a  DataSource based on Firestore
 *  param firebaseApp param textSearchController category export function useFirestoreDataSource ( { firebaseApp , textSearchController } ) { const firestoreRef = useRef < Firestore > ( ) ; const firestore = firestoreRef . current ; useEffect ( ( ) => { if ( ! firebaseApp ) return ; firestoreRef . current = getFirestore ( firebaseApp ) ; } , [ firebaseApp ] ) ; param doc param path param resolvedSchema category function createEntityFromSchema < M extends { [ Key ] : any ; } > ( doc , path , resolvedSchema ) { const values = firestoreToCMSModel ( doc . data ( ) , resolvedSchema , path ) ; const data = doc . data ( ) ? resolvedSchema . properties ? sanitizeData ( values as EntityValues < M > , resolvedSchema . properties , path ) : doc . data ( ) : undefined ; return { id : doc . id , path : getCMSPathFromFirestorePath ( doc . ref . path ) , values : data } ; } param fsPath function getCMSPathFromFirestorePath ( fsPath ) { let to = fsPath . lastIndexOf ( "/" ) ; to = to === - 1 ? fsPath . length : to ; return fsPath . substring ( 0 , to ) ; } /**
     * Recursive function that converts Firestore data types into CMS or plain
     * JS types.
     * FireCMS uses Javascript dates internally instead of Firestore timestamps.
     * This makes it easier to interact with the rest of the libraries and
     * bindings.
     * Also, Firestore references are replaced with  EntityReference param data param schema param path category function firestoreToCMSModel < M > ( data , schema , path ) { return traverseValues ( data , schema . properties , ( value , property ) => { if ( value === null ) return null ; if ( serverTimestamp ( ) . isEqual ( value ) ) { return null ; } if ( value instanceof Timestamp && property . dataType === "timestamp" ) { return value . toDate ( ) ; } if ( value instanceof GeoPoint && property . dataType === "geopoint" ) { return new GeoPoint ( value . latitude , value . longitude ) ; } if ( value instanceof DocumentReference && property . dataType === "reference" ) { return new EntityReference ( value . id , getCMSPathFromFirestorePath ( value . path ) ) ; } return value ; } ) ; } function buildQuery < M > ( path , filter , orderBy , order , startAfter , limit ) { if ( ! firestore ) throw Error ( "useFirestoreDataSource Firestore not initialised" ) ; const collectionReference = collection ( firestore , path ) ; const queryParams = [ ] ; if ( filter ) { Object . entries ( filter ) . filter ( ( [ _ , entry ] ) => ! ! entry ) . forEach ( ( [ key , filterParameter ] ) => { const [ op , value ] = filterParameter as [ WhereFilterOp , any ] ; queryParams . push ( whereClause ( key , op , value ) ) ; } ) ; } if ( filter && orderBy && order ) { Object . entries ( filter ) . forEach ( ( [ key , value ] ) => { if ( key !== orderBy ) { queryParams . push ( orderByClause ( key , "asc" ) ) ; } } ) ; } if ( orderBy && order ) { queryParams . push ( orderByClause ( orderBy , order ) ) ; } if ( startAfter ) { queryParams . push ( startAfterClause ( startAfter ) ) ; } if ( limit ) { queryParams . push ( limitClause ( limit ) ) ; } return query ( collectionReference , ... queryParams ) ; } function getAndBuildEntity < M > ( path , entityId , schema ) { if ( ! firestore ) throw Error ( "useFirestoreDataSource Firestore not initialised" ) ; return getDoc ( doc ( firestore , path , entityId ) ) . then ( ( docSnapshot ) => { const resolvedSchema = computeSchema ( { schemaOrResolver : schema , entityId : docSnapshot . id , path } ) ; return createEntityFromSchema ( docSnapshot , path , resolvedSchema ) ; } ) ; } async function performTextSearch < M > ( path , searchString , schema ) { if ( ! textSearchController ) throw Error ( "Trying to make text search without specifying a FirestoreTextSearchController" ) ; const ids = await textSearchController ( { path , searchString } ) ; if ( ! ids ) throw Error ( "The current path is not supported by the specified FirestoreTextSearchController" ) ; const promises = ids . map ( async ( entityId ) => { try { return await getAndBuildEntity ( path , entityId , schema ) ; } catch ( e ) { console . error ( e ) ; return null ; } } ) ; return Promise . all ( promises ) . then ( ( res ) => res . filter ( ( e ) => e !== null && e . values ) as Entity < M > [ ] ) ; } return { param path param schema param filter param limit param startAfter param searchString param orderBy param order return see useCollectionFetch category fetchCollection < M extends { [ Key ] : any ; } > ( { path , schema , filter , limit , startAfter , searchString , orderBy , order } ) { if ( searchString ) { return performTextSearch ( path , searchString , schema ) ; } console . debug ( "Fetching collection" , path , limit , filter , startAfter , orderBy , order ) ; const query = buildQuery ( path , filter , orderBy , order , startAfter , limit ) ; const resolvedSchema = computeSchema ( { schemaOrResolver : schema , path } ) ; return getDocs ( query ) . then ( ( snapshot ) => snapshot . docs . map ( ( doc ) => createEntityFromSchema ( doc , path , resolvedSchema ) ) ) ; } , param path param schema param onSnapshot param onError param filter param limit param startAfter param orderBy param order return see useCollectionFetch category listenCollection < M extends { [ Key ] : any ; } > ( { path , schema , filter , limit , startAfter , searchString , orderBy , order , onUpdate , onError } ) { console . debug ( "Listening collection" , path , limit , filter , startAfter , orderBy , order ) ; const query = buildQuery ( path , filter , orderBy , order , startAfter , limit ) ; if ( searchString ) { performTextSearch ( path , searchString , schema ) . then ( onUpdate ) . catch ( ( e ) => { if ( onError ) onError ( e ) ; } ) ; return ( ) => { } ; } const resolvedSchema = computeSchema ( { schemaOrResolver : schema , path } ) ; return onSnapshot ( query , { next : ( snapshot ) => { onUpdate ( snapshot . docs . map ( ( doc ) => createEntityFromSchema ( doc , path , resolvedSchema ) ) ) ; } , error : onError } ) ; } , param path param entityId param schema category fetchEntity < M extends { [ Key ] : any ; } > ( { path , entityId , schema } ) { return getAndBuildEntity ( path , entityId , schema ) ; } , param path param entityId param schema param onUpdate param onError return category listenEntity < M extends { [ Key ] : any ; } > ( { path , entityId , schema , onUpdate , onError } ) { if ( ! firestore ) throw Error ( "useFirestoreDataSource Firestore not initialised" ) ; return onSnapshot ( doc ( firestore , path , entityId ) , { next : ( docSnapshot ) => { const resolvedSchema = computeSchema ( { schemaOrResolver : schema , entityId : docSnapshot . id , path } ) ; onUpdate ( createEntityFromSchema ( docSnapshot , path , resolvedSchema ) ) ; } , error : onError } ) ; } , param path param entityId param values param schema param status category saveEntity : async function < M extends { [ Key ] : any ; } > ( { path , entityId , values , schema , status } ) : Promise < Entity < M > > { if ( ! firestore ) throw Error ( "useFirestoreDataSource Firestore not initialised" ) ; const resolvedSchema = computeSchema ( { schemaOrResolver : schema , entityId , path } ) ; const properties = resolvedSchema . properties ; const collectionReference = collection ( firestore , path ) ; const updatedFirestoreValues = updateAutoValues ( { inputValues : values , properties , status , timestampNowValue : serverTimestamp ( ) , referenceConverter : ( value ) => doc ( firestore , value . path , value . id ) , geopointConverter : ( value ) => new FirestoreGeoPoint ( value . latitude , value . longitude ) } ) ; console . debug ( "Saving entity" , path , entityId , updatedFirestoreValues ) ; let documentReference ; if ( entityId ) documentReference = doc ( collectionReference , entityId ) ; else documentReference = doc ( collectionReference ) ; return setDoc ( documentReference , updatedFirestoreValues , { merge : true } ) . then ( ( ) => ( { id : documentReference . id , path : documentReference . path , values : firestoreToCMSModel ( updatedFirestoreValues , resolvedSchema , path ) as EntityValues < M > } ) ) ; } , param entity param schema category async deleteEntity < M extends { [ Key ] : any ; } > ( { entity } ) { if ( ! firestore ) throw Error ( "useFirestoreDataSource Firestore not initialised" ) ; return deleteDoc ( doc ( firestore , entity . path , entity . id ) ) ; } , param path param name param value param property param entityId return category checkUniqueField ( path , name , value , property , entityId ? ) { if ( ! firestore ) throw Error ( "useFirestoreDataSource Firestore not initialised" ) ; console . debug ( "Check unique field entity" , path , name , value , entityId ) ; if ( property . dataType === "array" ) { console . error ( "checkUniqueField received an array" ) ; } if ( value === undefined || value === null ) { return Promise . resolve ( true ) ; } const q = query ( collection ( firestore , path ) , whereClause ( name , "==" , value ) ) ; return getDocs ( q ) . then ( ( snapshots ) => snapshots . docs . filter ( doc => doc . id !== entityId ) . length === 0 ) ; } } ; }