@anew/firestore

    0.6.2 • Public • Published

    Anew Firestore

    One of the common differences between Firebase, a NoSQL solution, and any other SQL solution is it's extremely flexible. That is, the shape of data is left to the user: where each document (data unit) may follow a different shape. This unbounded flexibility may very easily lead to bad design patterns, and even worse, broken code. This is why @anew/firestore was created: to ensure a strict shape for each document that lives under the same collection (data type).

    Updates

    For updates checkout Change Log.

    Installation

    To install @anew/firestore directly into your project run:

    npm i @anew/firestore -S
    

    for yarn users, run:

    yarn add @anew/firestore
    

    Table of Contents

    Collection

    A collection in firestore is similar to a table in a SQL database solution. Collections contain what are called documents (similar to rows in a SQL database solutions). You can view a collection as a data type where each collection follows a common pattern (shape) that differs from other collections. To define a collection you must define a class as follows:

    import firestore from 'path/to/firestore/instance'
    import { Collection } from '@anew/firestore'
    
    class CollectionName extends Collection {
        /**
         | ------------------------ 
         | Required
         | ------------------------
         */
        static collection = firestore.collection('collectionName')
    
        /**
         | ------------------------ 
         | Alternative 1
         | ------------------------
         | you may pass the firestore and collection name
         */
        static name = 'collectionName'
        static firestore = firestore
    
        /**
         | ------------------------ 
         | Alternative 2
         | ------------------------
         | if the name is not passed the class name will be automatically used. 
         | So in this case `static name = CollectionName` will be used.
         */
        static firestore = firestore
    }

    Other properties that you may define are:

    class CollectionName extends Collection {
        // ...
    
        /**
         | ------------------------ 
         | Shape
         | ------------------------
         | This property defines the shape of each document that will
         | live under this collection. This property is used to error
         | data that is added to this collection that does not follow 
         | the define shape. This guarantees that any document write/read
         | will have the define shape.
         |
         | The `Types` object used here is explained in detail below. 
         */
        static shape = {
            name: Types.string,
            date: Types.timestamp,
        }
    
        /**
         | ------------------------ 
         | Defaults
         | ------------------------
         | This property resolves document properties that are not defined.
         | In this example, adding a document without defining `name` will resolve
         | name to 'Guest'.
         */
        static defaults = {
            name: 'Guest',
        }
    }

    Add

    You may add/create a document as follows:

    class CollectionName extends Collection {
        // ...
    
        /**
         | Adding from a class method
         */
        someAddMethod() {
            this.add({
                name: 'Brian',
                date: new Date(),
            })
        }
    
        /**
         | Adding with reference
         */
        someAddWithReferenceMethod() {
            const newDocumentReference = this.ref()
    
            this.add(newDocumentReference, {
                name: 'Brian',
                date: new Date(),
            })
    
            /**
             | Alternatively
             | ------------------------
             | You may pass document id instead of reference
            */
            this.add('DocumentId', {
                name: 'Brian',
                date: new Date(),
            })
        }
    }
    
    const collectionName = new CollectionName()
    
    /**
     | Adding from a class instance
    */
    collectionName.add({
        name: 'Brian',
        date: new Date(),
    })

    Query

    The query method is similar to firebase's where method with slight differences. The first difference is that they may be used with actions other than get as well. These actions are get, update, and delete. You may create query as follows:

    class CollectionName extends Collection {
        // ...
    
        /**
         | The following query matches a single document's unique id/reference
        */
        idQuery() {
            this.query(documentId)
    
            // Alternative
            this.query(documentReference)
        }
    
        /**
         | You may pass an array of ids/references to get multiple document
        */
        idsQuery() {
            this.query(documentOneId, documentTwoId, ...documentIds)
            /* or */
            this.query(documentOneReference, documentTwoReference, ...documentReferences)
    
            // Alternative 1
            this.query([documentOneId, documentTwoId, ...documentIds])
            /* or */
            this.query([documentOneReference, documentTwoReference, ...documentReferences])
    
            // Alternative 2
            this.query(documentOneId)
                .query(documentTwoId)
                .query(...documentIds)
            /* or */
            this.query(documentOneReference)
                .query(documentTwoReference)
                .query(...documentReferences)
        }
    
        /**
         | The follow is a compound query that checks aganist multiple
         | document properties. The following query is a composition (AND) query
         | where only document that match all queries are selected.
        */
        compoundQuery() {
            this.query('name', '==', name).query('date', '<=', maxDate)
    
            // Alternative
            this.query([['name', '==', name], ['date', '<=', maxDate]])
        }
    }

    You may use queries with actions as follows:

    // Get all matched documents
    this.query('name', '==', name).query('date', '<=', maxDate).get()
    
    // Delete all matched documents
    this.query('name', '==', name).query('date', '<=', maxDate).delete()
    
    // Update all matched documents
    this.query('name', '==', name).query('date', '<=', maxDate).update({...})

    Each method used here is explained in detail below.

    Get

    The get method is used to retrieve documents from firebase. You may use the get method as follows:

    // The get method takes two optional arguments a query and a reducer.
    // The query argument is used to get only document that match the passed query.
    // The reducer argument is used to reduce each document retrieved to a new value.
    this.get(query, reducer)
    
    // Alternative
    this.query(query).get(reducer)

    Update

    The update method is used to modify documents in firebase. You may use the update method as follows:

    this.update(query, update)
    
    // Alternative
    this.query(query).update(update)

    Delete

    The delete method is used to remove documents from firebase. You may use the delete method as follows:

    this.delete(query)
    
    // Alternative
    this.query(query).delete()

    Utilities

    /**
     | Generate a Document Reference
     | @param {String} id You may OPTIONALLY pass a specific id for the reference
    */
    this.ref(id)
    
    /**
     | Generate a Document Id
    */
    this.id()
    
    /**
     | Convert { lat, long } to GeoPoint object
     | @param {Number} options.lat  Latitude
     | @param {Number} options.long Longitude
    */
    this.geopoint({ lat, long })
    
    /**
     | Alternatively
     | -----------------------
     | You may use the GeoPoint class
    */
    new Collection.GeoPoint(lat, long)
    
    /**
     | Convert Date object to Timestamp object
     | @param {Date} date  JavaScript Date Object
    */
    this.timestamp(date)
    
    /**
     | Alternatively
     | -----------------------
     | You may use the Timestamp class
    */
    new Collection.Timestamp(date)

    Listeners

    The following listeners may be defined as follows:

    class CollectionName extends Collection {
        // ...
    
        // The onRead listeners is applied to all `get/watch` calls.
        // It is a global reducer that changes all documents retrieved.
        onRead(data) {
            return {
                ...date,
                newProperty: true,
            }
        }
    
        // The onWrite listener is applied to all `update/add` calls.
        // It is a global reducer that changes data before it is written to a document.
        onWrite(data) {
            if (data.location) {
                data = {
                    ...data,
                    // convert to geolocation before write
                    location: this.geopoint(data.location),
                }
            }
    
            return data
        }
    
        // You may also define a listener per write action
        onAdd(data) {...}
        onUpdate(data) {...}
    }

    Types

    The Types object is used to define the shape property in a Collection. These type checks may also be used in the application.

    import { Types } from '@anew/firestore'
    
    // Check if argument is a boolean
    Types.boolean(arg)
    
    // Check if argument is null
    Types.null(arg)
    
    // Check if argument is a string
    Types.string(arg)
    
    // Check if argument is a number
    Types.number(arg)
    
    // Check if argument is a between a range
    Types.number.range(0, 5)(arg)
    
    // Check if argument is an object (also known as map in firebase)
    Types.map(arg)
    
    // Check if argument is an array
    Types.array(arg)
    
    // Check if argument is an array of strings
    Types.array.of(Types.string)(arg)
    
    // Check if argument is a timestamp
    Types.timestamp(arg)
    
    // Check if argument is a geopoint
    Types.geopoint(arg)
    
    // Check if argument is a document reference specific to `collectionName`
    Types.ref('collectionName')(arg)
    
    // Check if argument is a document reference without a specifying a collection
    Types.ref.any(arg)
    
    // Check if argument is an object that contains the passed properties with their types.
    Types.shape({
        name: Types.string,
        price: Types.number,
    })(arg)
    
    // Check if argument matches one of the passed values
    Types.oneOf([1, 'One'])(arg)
    
    // Check if argument passes one of the types in the list
    Types.oneOfType([Types.string, Types.number])(arg)

    Example

    The following is a basic application of @anew/firestore.

    Basic Firebase Setup

    // src/firebase.js
    import firebase from 'firebase/app'
    import 'firebase/firestore'
    
    const app = firebase.initializeApp({
        apiKey: process.env.REACT_APP_API_KEY,
        authDomain: process.env.REACT_APP_AUTH_DOMAIN,
        databaseURL: process.env.REACT_APP_DATABASE_URL,
        projectId: process.env.REACT_APP_PROJECT_ID,
        storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
        messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
    })
    
    export const firestore = app.firestore()

    Company Collection

    // src/company.js
    import { firestore } from './firebase' // setup file above
    import { Types, Collection } from '@anew/firestore'
    
    class Company extends Collection {
        static collection = firestore.collection('company')
    
        static shape = {
            name: Types.string,
            margin: Types.number,
        }
    }
    
    export default new Company()

    Product Collection

    // src/product.js
    import Company from './company' // company file above
    import { firestore } from './firebase' // setup file above
    import { Types, Collection } from '@anew/firestore'
    
    class Product extends Collection {
        static collection = firestore.collection('product')
    
        static shape = {
            name: Types.string,
            price: Types.number,
            location: Types.geopoint,
            dateAdded: Types.timestamp,
            discount: prop => {
                return prop < 50
            },
            company: Types.shape({
                name: Types.string,
                ref: Types.ref(Company),
            }),
        }
    
        static defaults = {
            discount: 0,
            price: 0,
            location: {
                lat: 0,
                long: 0,
            },
            dateAdded: () => {
                return new Date()
            },
        }
    
        /*
        | ----------------
        | Listeners
        | ----------------
        */
    
        onRead({ dateAdded, location, ...data }) {
            return {
                ...data,
                dateAdded: dateAdded.toDate(),
                location: {
                    lat: location.latitude,
                    long: location.longitude,
                },
            }
        }
    
        onAdd({ location, ...data }) {
            return {
                ...data,
                location: new this.types.GeoPoint(location.lat, location.long),
            }
        }
    
        onUpdate({ location, ...data }) {
            if (location) {
                return {
                    ...data,
                    location: new this.types.GeoPoint(location.lat, location.long),
                }
            }
    
            return data
        }
    
        /*
        | ----------------
        | Reducers
        | ----------------
        */
    
        async detailedReducer(data) {
            return {
                ...data,
                company: await Company.get(data.company.ref),
            }
        }
    
        /*
        | ----------------
        | Getters
        | ----------------
        */
    
        getWithDetails(id) {
            return this.query(id).get(this.detailedReducer)
        }
    
        /*
        | ----------------
        | Setters
        | ----------------
        */
    
        async updatePrice(data) {
            let { company } = data
    
            if (!company.margin) {
                company = await data.company.ref.get()
            }
    
            return await this.query(data.id).update({
                price: data.cost * company.margin,
            })
        }
    
        updateByName(name, update) {
            return this.query('name', '==', name).update(update)
        }
    
        deleteAllExpired(expiredDate) {
            return this.query('date', '<=', expiredDate).delete()
        }
    }
    
    export default new Product()

    Install

    npm i @anew/firestore

    DownloadsWeekly Downloads

    0

    Version

    0.6.2

    License

    MIT

    Unpacked Size

    38 kB

    Total Files

    15

    Last publish

    Collaborators

    • abubakir1997