/* global localStorage, navigator, window */ import { createMethodSignature, GUID, Schema } 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: 'offlineFirst', 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 = 'offlineFirst', useWorker = false, schemas = {} } = {}) { super() this.#_name = name this.#_dataStrategy = dataStrategy this.#_useWorker = useWorker this.#_schemas = schemas this.#_started = false this.#_guid = GUID() this.#_models = {} this.#_useWorker = useWorker || false this.#_workers = {} this.localDatabaseTransport = new LocalDatabaseTransport() this.#_tabId = GUID() // 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 } #setModel(entity = '', dataEntity = {}) { let _error = null let _data = null 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 } #mapModels(schemas) { let _error = null let _data = null try { 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.#setModel(entity, dataEntity) } } _data = this.#_models } catch (error) { console.error(error) _error = error } 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 GUID to localStorage * @param {string} guid * @return Foundation GUID saved on localStorage */ setGuidStorage (guid) { window.localStorage.setItem('guid', guid) return window.localStorage.getItem('guid') } /** * @Method Foundation.setupAppGuid * @description check if Foundation has a GUID saved o * @return Foundation GUID 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)) }) } }) } */ /** * @Private * @description Starts foundation stack required items * @return {object} signature - Default methods signature format { error, data } * @return {string|object} signature.error - Execution error * @return {object} signature.data - Foundation data */ async #startVitals () { let _error = null let _data = null try { this.setupAppGuid() const mapModels = this.#mapModels(this.#_schemas) await this.localDatabaseTransport.connect() // start database // start all here _data = { status: { mapModels } } } catch (error) { console.error(error) _error = error } this.triggerEvent('foundation:startVitals', { foundation: this, error: _error, data: _data }) return createMethodSignature(_error, _data) } /** * @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 { const vitals = await this.#startVitals() // if (this.useWorker) { // await this.#registerApplicationWorker() // } this.#_started = true _data = { ...vitals.data, started: this.#_started } } catch (error) { console.error(error) _error = error } this.triggerEvent('foundation:start', { foundation: this, error: _error, data: _data }) // console.warn('STARTED>>>>>>>>>>>', this) return createMethodSignature(_error, _data) } }