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