Source: Foundation.js

/* global localStorage, navigator, window */
import { createMethodSignature, uuid, Schema, genDbName } from './utils'
import DataEntity from './DataEntity'
import LocalDatabaseTransport from './LocalDatabaseTransport'
import EventSystem from './EventSystem'

// import workerOnMessage from './events/workerOnMessage'

/**
 * @author Eduardo Perotta de Almeida <web2solucoes@gmail.com>
 * @Class Foundation
 * @description Foundation boostrap class
 * @extends EventSystem
 * @param  {object} config - Foundation configuration
 * @param  {string} config.name - Foundation name
 * @param  {string} config.dataStrategy - Data strategy. Recognized values: offlineFirst, onlineFirst, offline, online
 * @param  {boolean} config.useWorker - Use a ServiceWorker in Background
 * @param  {object}  config.schemas - map of data schemas
 * @example {@lang javascript}
// =========> main.js
// import React
import React from 'react'
import ReactDOM from 'react-dom'

// import Bootstrap
import 'bootstrap/dist/css/bootstrap.css'

// import React app
import App from './App'

// import agnostic foundation foundation class
import Foundation from './foundation/Foundation'

const CustomerSchema = new Foundation.Schema({
    name: {
        type: String,
        required: true,
        index: true
    },
    address: {
        type: String,
        required: true,
        index: true
    },
    email: {
        type: String,
        required: true,
        index: true
    },
    cards: {
        type: [],
        required: true
    }
})

const OrderSchema = new Foundation.Schema({
    name: {
        type: String,
        required: true,
        index: true
    },
    shipTo: {
        type: String,
        required: true,
        index: true
    },
    paymentMethod: {
        type: String,
        required: true,
        index: true
    },
    amount: {
        type: Number,
        required: true,
        default: 0,
        index: true
    },
    date: {
        type: Date,
        default: Date.now,
        index: true
    }
})

const ProductSchema = new Foundation.Schema({
    name: {
        type: String,
        required: true,
        index: true
    },
    vendor: {
        type: String,
        required: true,
        index: true
    },
    price_cost: {
        type: Number,
        required: true,
        default: 0,
        index: true
    }
})

const UserSchema = new Foundation.Schema({
    name: {
        type: String,
        required: true
    },
    username: {
        type: String,
        required: true
    }
})

const foundation = new Foundation({
    name: 'My App',
    useWorker: true,
    dataStrategy: 'offline',
    schemas: {
        User: UserSchema,
        Product: ProductSchema,
        Order: OrderSchema,
        Customer: CustomerSchema
    }
})

foundation.on('foundation:start', async function(eventObj) {
    const {
        foundation,
        error
    } = eventObj
    if (error) {
        throw new Error(`Error starting foundation stack: ${error}`)
    }
    const {
        User,
        Product
    } = foundation.data
    const Eduardo = await User.add({
        name: 'Eduardo Almeida',
        username: 'web2'
    })
    console.debug('Eduardo', Eduardo)

    const Volvo = await Product.add({
        name: 'Volvo XC90',
        vendor: 'Volvo',
        price_cost: 150000
    })
    console.debug('Volvo', Volvo)
})

// start foundation and get it ready to be used
await foundation.start()

const start = await foundation.start()
if (start.error) {
    throw new Error(`Error starting foundation stack: ${start.error}`)
}
// console.debug('start', start)
ReactDOM.render(
  <App foundation={foundation} />,
  document.getElementById('root')
)
 */
export default class Foundation extends EventSystem {
  
  #_schemas
  #_name
  #_dataStrategy
  #_started
  #_models
  #_guid
  #_useWorker
  #_workers
  #_tabId

  constructor ({
    name = 'My Foundation Name',
    dataStrategy = 'offline',
    useWorker = false,
    schemas
  }) {
    super()
    this.#_name = name
    this.#_dataStrategy = dataStrategy
    this.#_useWorker = useWorker
    this.#_schemas = schemas
    this.#_started = false
    this.#_guid = uuid()
    this.#_models = {}
    this.#_useWorker = useWorker || false
    this.#_workers = {}
    this.localDatabaseTransport = new LocalDatabaseTransport({
      dbName: genDbName(name)
    })
    this.#_tabId = uuid() // assume new Id on every refresh
  }

  /**
   * @member {getter} Foundation.dataStrategy
   * @Description Get the data strategy being used.<br> Possible values are: offlineFirst, onlineFirst, offline, online. <br> Default: offlineFirst
   * @example console.log(Foundation.dataStrategy)
   * @return {string} this.#_dataStrategy
   */
  get dataStrategy () {
    return this.#_dataStrategy
  }

  /**
   * @member {getter} Foundation.guid
   * @description Get the Foundation Session guid currently being used.
   * @example console.log(Foundation.guid)
   */
  get guid () {
    return this.#_guid
  }

  /**
   * @member {getter} Foundation.data
   * @description Get the Foundation data API(DataEntity)
   * @example 
      const { User, Product } = foundation.data
      const Eduardo = await User.add({
        name: 'Eduardo Almeida',
        username: 'web2'
      })
      console.debug(Eduardo)
      // {  
      //    data: {__id: 1, _id: "600e0ae8d9d7f50000e1444b", name: "Eduardo Almeida", username: "web2", id: "600e0ae8d9d7f50000e1444b"}
      //    error: null
      // }
   */
  get data() {
    return this.#_models
  }

  /**
   * @member {getter} Foundation.tabId
   * @description Get the Browser tab ID
   * @example 
      console.log(foundation.tabId)
   */
  get tabId() {
    return this.#_tabId
  }

  /**
   * @member {getter} Foundation.name
   * @name Foundation.name
   * @description Get the Foundation name
   * @example console.log(Foundation.name)
   */
  get name () {
    return this.#_name
  }

  /**
   * @member {setter} Foundation.name
   * @name Foundation.name
   * @description Set the Foundation name
   * @example Foundation.name = 'Provide the name here'
   * @param  {string} name - Foundation name
   */
  set name (name) {
    this.#_name = name
  }

  /**
   * @member {getter} Foundation.started
   * @description Get the start state
   * @example console.log(Foundation.started)
   */
  get started () {
    return this.#_started
  }

  /**
   * @memberof Foundation
   * @member {getter} Foundation.applicationWorker
   * @example Foundation.applicationWorker.postMessage()
   * @description Get the Foundation worker
   */
  get applicationWorker() {
    return this.#_workers.foundation
  }

  /**
   * @Method Foundation.mapToDataEntityAPI
   * @summary Maps an Data Entity abstraction to foundation Data API
   * @description An Data Entity abstraction is an instance of the {@link DataEntity}. 
   * Once it is mapped to foundation Data API, you can reach every Data Entity in the system from a single source point.
   * This method dont works as expected if  you call it after {@link Foundation.start} method.
   * See {@link Foundation.importDataEntity} for usage further information.
   * @param  {string} entity - Data Entity name
   * @param  {dataEntity} dataEntity - An {@link DataEntity} instance
   */
  mapToDataEntityAPI(entity, dataEntity) {
    let _error = null
    let _data = null
    // if call mapToDataEntityAPI('Product') more than once, it will ovewrite the previous set Product model
    this.#_models[entity] = dataEntity
    _data = this.#_models[entity]
    return createMethodSignature(_error, _data)
  }
  
  /**
   * @memberof Foundation
   * @member {getter} Foundation.Schema
   * @example new Foundation.Schema({})
   * @description Creates new data schema
   * @returns schema creator
   */
  static get Schema() {
    return Schema
  }

  /**
   * @Method Foundation.importDataEntity
   * @summary Alias to Foundation.mapToDataEntityAPI(entity = '', dataEntity = {})
   * @description An Data Entity abstraction is an instance of the {@link DataEntity}. 
   * Once it is mapped to foundation Data API, you can reach every Data Entity in the system from a single source point.
   * This method dont works as expected if  you call it after {@link Foundation.start} method
   * @param  {object} spec - Data Entity abstraction specification
   * @param  {string} spec.entity - Data Entity name
   * @param  {dataEntity} spec.dataEntity - An {@link DataEntity} instance for the entity defined on `spec.entity`
   * @example
const productSchema = new Foundation.Schema({
  name: {
    type: String,
    required: true,
    index: true
  },
  vendor: {
    type: String,
    required: true,
    index: true
  },
  price: {
    type: Number,
    required: true,
    index: true
  }
})

// start the foundation
const foundation = new Foundation({
  name: 'My Test app',
  schemas: {
    // Customer: schema
  }
})

// Build a customized Data Entity abstraction
const MyCustomizedDataEntity = class extends DataEntity {
  constructor (config) {
    super(config)
  }

  sell (primaryKey, orderId) {
    // primaryKey is Product primary key value
    // orderId is the primaryKey of an Order
    // const foundOrder = await Order.findById(orderId)
    // if (foundOrder.error) {
    //  CAN NOT TO SELL
    // }
    // const items = foundOrder.data.lineItems.filter(i => (i.productId === primaryKey))
    // If  Order has the product listed item
    // if(items[0])
    // {
    //    await this.delete(primaryKey) // deletes a Product from Products
    // }
  }
}

// instance of the custimized Data Entity
const productDataEntity = new MyCustomizedDataEntity({
  foundation,
  entity: 'Product',
  schema: productSchema
})

// import data entity
foundation.importDataEntity({
  entity: 'Product',
  dataEntity: productDataEntity
})

// start the foundation
await foundation.start()

// you can now do things like:

const { Product } = foundation.data

await Product.add({
  name: 'Big Mac',
  vendor: 'McDonalds',
  price: 3
})

   */
  importDataEntity({ entity, dataEntity}) {
    this.mapToDataEntityAPI(entity, dataEntity)
  }

  #mapModels(schemas) {
    let _error = null
    let _data = null
    for (const entity in schemas) {
      if (Object.prototype.hasOwnProperty.call(schemas, entity)) {
        // console.debug('for (const entity in schemas)', entity)
        const strategy = 'offlineFirst'
        const schema = schemas[entity]
        const dataEntity = new DataEntity({
          foundation: this,
          entity,
          strategy,
          schema
        })
        this.mapToDataEntityAPI(entity, dataEntity)
      }
    }
    // _data = this.#_models
    // return createMethodSignature(_error, _data)
  }

  /**
   * @member {getter} Foundation.useWorker
   * @Description flag if is there ServiceWorker being used
   * @return  {boolean}
   */
  get useWorker () {
    return this.#_useWorker
  }

  /**
   * @Method Foundation.setGuidStorage
   * @description save Foundation uuid to localStorage
   * @param  {string} guid
   * @return Foundation uuid saved on localStorage
   */
  setGuidStorage (guid) {
    window.localStorage.setItem('guid', guid)
    return window.localStorage.getItem('guid')
  }

  /**
   * @Method Foundation.setupAppGuid
   * @description check if Foundation has a uuid saved o
   * @return Foundation uuid saved on localStorage
   */
  setupAppGuid () {
    const guidCache = window.localStorage.getItem('guid') || false
    if (guidCache) {
      this.#_guid = guidCache
    } else {
      this.setGuidStorage(this.#_guid)
    }
    return window.localStorage.getItem('guid')
  }
  
  /**
   * @async
   * @Method Foundation.#registerApplicationWorker
   * @description Setup and Register the main Service worker used by foundation core
   * @return  {object} signature - Default methods signature format { error, data }
   * @return  {string|object} signature.error - Execution error
   * @return  {object} signature.data - Worker Registration Object
   */
  /* #registerApplicationWorker (workerFile = 'ServiceWorker.js') {
    const self = this
    return new Promise((resolve, reject) => {
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker
          .register('/' + workerFile, {
            // scope: '/'
          })
          .then(function (reg) {
            // registration worked
            navigator.serviceWorker.addEventListener('message', workerOnMessage.bind(self))
            if (reg.installing) {
              self.#_workers['foundation'] = reg.installing
              self.#_workers['foundation'].postMessage({ cmd: 'getClientId', message: null })
            } else if (reg.active) {
              self.#_workers['foundation'] = reg.active
              self.#_workers['foundation'].postMessage({ cmd: 'getClientId', message: null })
            }
            resolve(createMethodSignature(null, reg))
          })
          .catch(function (error) {
            // registration failed
            resolve(createMethodSignature(error, null))
          })
      }
    })
  } */

  /**
   * @async
   * @Method Foundation.#registerWorker
   * @description Setup and Register a Service worker and get it ready for usage into your application scope
   * @param  {string} name - Worker name. Used to access it from the namespace
   * @param  {string} workerFile - Worker file name
   * @return  {object} signature - Default methods signature format { error, data }
   * @return  {string|object} signature.error - Execution error
   * @return  {object} signature.data - Worker Registration Object
   */
  /* #registerWorker (name = '', workerFile = 'ServiceWorker.js') {
    const self = this
    return new Promise((resolve, reject) => {
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker
          .register('/' + workerFile, {
            // scope: '/'
          })
          .then(function (reg) {
            // registration worked
            navigator.serviceWorker.addEventListener('message', workerOnMessage.bind(self))
            if (reg.installing) {
              self.#_workers[name] = reg.installing
              self.#_workers[name].postMessage({ cmd: 'getClientId', message: null })
            } else if (reg.active) {
              self.#_workers[name] = reg.active
              self.#_workers[name].postMessage({ cmd: 'getClientId', message: null })
            }
            resolve(createMethodSignature(null, reg))
          })
          .catch(function (error) {
            // registration failed
            resolve(createMethodSignature(error, null))
          })
      }
    })
  } */

  
  /**
   * @async
   * @Method Foundation.start
   * @description Starts foundation stack and get it ready to use. <br> it calls this.#startVitals() internally 
   * @return  {object} signature - Default methods signature format { error, data }
   * @return  {string|object} signature.error - Execution error
   * @return  {object} signature.data - Foundation data
   */
  async start () {
    let _error = null
    let _data = null
    try {
      this.setupAppGuid()
      const mapModels = this.#mapModels(this.#_schemas)
      
      const connection = await this.localDatabaseTransport.connect()

      if (connection.error) {
        _error = connection.error
      } else {
        this.#_started = true
        _data = {
          status: {
            mapModels,
            connection
          },
          started: this.#_started
        }
      }
      
    } catch (error) {
      _error = error
      _data = null
    }

    this.triggerEvent('foundation:start', {
      foundation: this,
      error: _error,
      data: _data
    })
    // console.warn('STARTED>>>>>>>>>>>', this)
    return createMethodSignature(_error, _data)
  }
}