UNPKG

@cocreate/crud-client

Version:

An useful CRUD api operate Create, read, update, delete with built in database. Can be used as a firebase alternative. Easily configured using HTML5 attributes and/or JavaScript API.

460 lines (423 loc) 12.9 kB
/******************************************************************************** * Copyright (C) 2023 CoCreate and Contributors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. ********************************************************************************/ /** * Commercial Licensing Information: * For commercial use of this software without the copyleft provisions of the AGPLv3, * you must obtain a commercial license from CoCreate LLC. * For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app. */ /* global CoCreate, CustomEvent */ (function (root, factory) { if (typeof define === "function" && define.amd) { define([ "@cocreate/socket-client", "@cocreate/indexeddb", "@cocreate/utils" ], function ( CoCreateSocket, indexeddb, { ObjectId, getValueFromObject, getAttributeNames, setAttributeNames, isValidDate } ) { return factory( true, CoCreateSocket, (indexeddb = indexeddb.default), { ObjectId, getValueFromObject, getAttributeNames, setAttributeNames, isValidDate } ); }); } else if (typeof module === "object" && module.exports) { const CoCreateSocket = require("@cocreate/socket-client"); const { ObjectId, getValueFromObject, getAttributeNames, setAttributeNames, isValidDate } = require("@cocreate/utils"); module.exports = factory(false, CoCreateSocket, null, { ObjectId, getValueFromObject, getAttributeNames, setAttributeNames, isValidDate }); } else { root.returnExports = factory( true, root["@cocreate/socket-client"], root["@cocreate/indexeddb"], root["@cocreate/utils"] ); } })( typeof self !== "undefined" ? self : this, function ( isBrowser, CoCreateSocket, indexeddb, { ObjectId, getValueFromObject, getAttributeNames, setAttributeNames, isValidDate } ) { const CoCreateCRUD = { socket: CoCreateSocket, /** * Performs a crud action using the define method . * * @see https://cocreate.app/docs/objects.html#send * @param method crud operation. * @param storage string or array of strings representing the storage name(s). * @param database string or array of strings representing the database name(s). * @param array string or array of strings representing the array name(s). * @param object object or array of objects * @param key key with in an object, supports dotNotation. * @return { Promise } The data read from the defined db's. Errors are logged and can be found in the data object * @throws \method failed */ send: function (data) { return new Promise(async (resolve, reject) => { if (!data) return resolve(null); if (!data.method) { // TODO: errorhandler(data, 'data.method is required') return resolve(data); } data.timeStamp = new Date().toISOString(); if (data.method.endsWith(".read")) data.broadcast = data.broadcastBrowser = false; if (!data.organization_id) data.organization_id = await this.socket.organization_id(); if (data.database || data.array || data.type) { if (!data.storage) data["storage"] = ["indexeddb", "mongodb"]; // TODO: if (!data.database, config.database) config will work in client and server or localStorage.database if (!data.database) data["database"] = data.organization_id; } let response; if ( isBrowser && indexeddb && data["storage"].includes("indexeddb") ) { response = await indexeddb.send(data); let type = data.method.split(".")[0]; if ( data.status !== "await" && type && response && response[type] && response[type].length ) { if ( type === "object" && response.object.length === 1 && !response.object[0].organization_id ) { } else { resolve(response); response.status = "resolve"; response.resolved = true; this.socket.send(response); return; } } else if ( data.status !== "await" && (typeof data["storage"] === "string" || (Array.isArray(data["stoarge"]) && data["storage"].length > 1)) ) { return resolve(response); } } if (!response || response.status !== "resolve") { this.socket.send(response || data).then((response) => { resolve(response); }); } }); }, listen: function (method, callback) { // TODO: this.socket.listen('crud.' + method, callback); this.socket.listen(method, callback); }, syncListeners: function () { const type = [ "storage", "database", "array", "index", "object" ]; const method = ["create", "read", "update", "delete"]; for (let i = 0; i < type.length; i++) { for (let j = 0; j < method.length; j++) { const action = type[i] + "." + method[j]; const self = this; this.listen(action, function (data) { self.sync(data); }); } } }, sync: async function (data) { if ( indexeddb && data.uid && data.status && data.status == "received" && !data.synced ) { if ( (data.host && data.host.startsWith("dev.")) || data.host.startsWith("test.") ) data.database = data.organization_id; if (data.method.endsWith(".read")) { if (this.socket.has(data.socketId)) { const self = this; let type = data.method.split(".")[0]; if (!data[type] || !data[type].length) return; // let deletedItems = await this.getDeletedItems() let isDeleted = ""; // this.isDeleted(type, items[i], deletedItems) if (isDeleted) { console.log( "sync failed item recently deleted" ); } else { for (let i = 0; i < data[type].length; i++) { let key, value; if ( data[type][i].modified && data[type][i].modified.on ) { key = "modified.on"; value = data[type][i].modified.on; } else if ( data[type][i].created && data[type][i].created.on ) { key = "created.on"; value = data[type][i].created.on; } else continue; let response = { clientId: data.clientId, frameId: data.frameId, socketId: data.socketId, method: type + ".read", user_id: data.user_id, organization_id: data.organization_id }; if (type === "object") { response[type] = { _id: data[type][i]._id }; response.array = data.array; } else { response[type] = data[type][i].name; } response = await indexeddb.send(response); let objLength = Object.keys(response[type][0]).length if ( !response[type].length || (type === "object" && response[type][0] && response[type][0]._id && objLength === 1) ) { response.method = type + ".create"; response[type] = data[type][i]; response = await indexeddb.send( response ); self.socket.sendLocalMessage(response); } else { let queryValue = value; let dataValue = getValueFromObject( response[type][0], key ); if ( isValidDate(queryValue) && isValidDate(dataValue) ) { queryValue = new Date(queryValue); dataValue = new Date(dataValue); } else if ( !response[type][0].organization_id ) { console.log("invalid date"); } if (dataValue < queryValue) { response.method = type + ".update"; response[type] = data[type][i]; response = await indexeddb.send( response ); self.socket.sendLocalMessage( response ); } } } } } } else if ( this.socket.clientId != data.clientId || data.updateDB ) { data.synced = true; // TODO: if database was updated due to host and environment handling if (data.updateDB) data.database = data.organization_id; // TODO: returned from server socket.send authorize perhaps requires a flag so that it can be removed after autorization.. if ( data.$filter && data.$filter.query && data.$filter.query._id && data.$filter.query._id.$eq === "$user_id" ) data.$filter.query._id.$eq = this.socket.user_id; this.socket.sendLocalMessage(data); indexeddb.send({ ...data }); } } }, syncServer: function () { const self = this; const promise = indexedDB.databases(); promise.then((databases) => { for (let database of databases) { if ( !["socketSync", "crudSync"].includes(database.name) ) { let dbRequest = indexedDB.open(database.name); dbRequest.onsuccess = function () { let db = dbRequest.result; let objectStoreNames = Array.from( db.objectStoreNames ); let arrayLength = objectStoreNames.length; for (let array of objectStoreNames) { let transaction = db.transaction( [array], "readonly" ); let objectStore = transaction.objectStore(array); let objectRequest = objectStore.getAll(); objectRequest.onsuccess = function () { arrayLength -= 1; let data = { method: "syncServer", database: database.name, array, object: objectRequest.result }; self.socket.send(data); console.log("sync success", { database: database.name, array }); if (!arrayLength && db.close) { db.close(); console.log("sync completed"); } }; objectRequest.onerror = function () { console.log("sync failed", { database: database.name, array }); if (!arrayLength && db.close) { db.close(); console.log("sync completed"); } }; } }; dbRequest.onerror = function () { console.log(openRequest.error, database); }; } } }); }, ObjectId, getValueFromObject, getAttributeNames, setAttributeNames }; if (isBrowser) { CoCreateCRUD.syncListeners(); let attributes = { // attribute | variable host: "host", organization_id: "organization_id", apikey: "apikey", storage: "storage", database: "database", array: "array", // array: 'array', // table: 'array', object: "object", // document: 'object', // row: 'object', key: "key", // property: 'key', // name: 'key', updateName: "updateName", index: "index", crud: "isCrud", crdt: "isCrdt", realtime: "isRealtime", save: "isSave", update: "isUpdate", delete: "isDelete", upsert: "isUpsert", read: "isRead", listen: "isListen", broadcast: "broadcast", "broadcast-sender": "broadcastSender", "broadcast-browser": "broadcastBrowser", room: "room", state_id: "state_id", "state-overwrite": "stateOverwrite" }; if (!window.CoCreateConfig) window.CoCreateConfig = { attributes }; else if (!window.CoCreateConfig.attributes) window.CoCreateConfig.attributes = attributes; else setAttributeNames(attributes, false); } return CoCreateCRUD; } );