@fto-consult/common
Version:
Un ensemble de bibliothèques et d'utilistaires communs pour le développement d'applications javascript
565 lines (546 loc) • 22.6 kB
JavaScript
/*** fix IE 11 : fetch is undefined with fetch polyfill @see : https://github.com/github/fetch */
import 'whatwg-fetch'
import {isElectron} from "$cplatform";
import {extendObj,isNonNullString,defaultStr,isPromise,isObj,urlJoin,defaultObj,parseURI} from "$cutils";
import DateLib from "$date";
import {getDBName,getDBNamePrefix,sanitizeDBName,parseDBName,isDocUpdate,isTableLocked,POUCH_DATABASES} from "../utils";
import actions from "$cactions";
import { sanitizeName,isCommon } from '../dataFileManager/utils';
import { getLoggedUser,getLoginId} from '$cauth/utils';
import {structDataDBName } from "../dataFileManager/structData";
import {triggerEventTableName} from "../dataFileManager/structData";
import isDataFileDBName from '../dataFileManager/isDataFileDBName';
import PouchDBObj from './pouchdb';
const {PouchDB,...pouchdbRest} = PouchDBObj;
import isValidDataFile from '../dataFileManager/isValidDataFile';
import localServer from "./localServerConfig";
const TRIGGER_CHANGES_DBS = {};
if(typeof window !=='undefined' && window && !window?.hasSetPouchEventsGetDB){
window.hasSetPouchEventsGetDB = true;
PouchDB.on('destroyed', function (dbName) {
// called whenever a db is destroyed.
dbName = sanitizeName(dbName.ltrim(getDBNamePrefix()));
const DATABASES = POUCH_DATABASES.get();
APP.trigger(APP.EVENTS.REMOVE_POUCHDB_DATABASE,dbName);
if(DATABASES[dbName]){
let db = DATABASES[dbName];
if(db.changesResult && isFunction(db.changesResult.cancel)){
db.changesResult.cancel();
}
DATABASES[dbName] = db.changes = null;
db = null;
}
if(typeof gc !=='undefined' && typeof gc =='function'){
gc();
}
});
}
//upsert plugin
export function upsertInner(db, docId, diffFun,options) {
return db.get(docId).catch(function (err) {
/* istanbul ignore next */
if (err.status !== 404) {
throw err;
}
return {};
//return err;
}).then(function (doc) {
let docRev = doc._rev;
let newDoc = diffFun(doc,docId);
if (!isObj(newDoc)) {
// if the diffFun returns falsy, we short-circuit as
// an optimization
return { updated: false, rev: docRev, id: docId };
}
for(let i in newDoc){
if((i.startsWith("_") && i !== '_deleted') || (Array.isArray(newDoc[i]) && newDoc[i].length <= 0)){
delete newDoc[i];
}
}
newDoc.dataFileType = defaultStr(newDoc.dataFileType,db.infos?.type).toLowerCase();
newDoc._id = docId;
newDoc._rev = docRev;
addDefaultFields(newDoc,options);
newDoc.dbId = defaultStr(newDoc.dbId,db.realName,undefined);
//end ms-update;
return tryAndPut(db, newDoc, diffFun,options)
});
}
/** ms-update, add options parameters : options représente les options à passer lors
* de l'enregistrement de la mise à jour d'une bd pouchdb
*
*/
export function tryAndPut(db, doc, diffFun,options) {
if(!isObj(options) ){
options = {};
}
return db.put(doc,options).then(function (res) {
doc._rev = res.rev;
return {
updated: true,
_rev: res.rev,
_id: doc._id,
doc : doc,
newDoc : doc
};
}).catch(function (err) {
/* istanbul ignore next */
if (err.status !== 409) {
throw err;
}
return upsertInner(db, doc._id, diffFun,options);
});
}
/*** ajoute les champs par défaut au moment de la mise à jour d'un document
* l'on peut décider de modifier où de ne pas modifier les
* date de mise à jour, les dates de modification et la personnes qui a modifié le document
*/
export function addDefaultFields(newDoc,options){
///l'on peut dorénavant interdire l'ajout de certains champs par défaut lors de la mise à jour des données par défaut aux données lors de l'insertion
options = isObj(options)? options : {};
let user = getLoggedUser();
if(user){
const loginId = getLoginId(user);
const hasLoginId = isNonNullString(loginId) || typeof loginId =='number';
newDoc.createdBy = defaultStr(newDoc.createdBy) || loginId;
if(options.updatedBy !== false || !isNonNullString(newDoc.updatedBy)){
newDoc.updatedBy = hasLoginId ? loginId : defaultStr(newDoc.updatedBy)
}
}
//ms-update prerequise update for all type of field db
let date = DateLib.SQLDate();
let time = DateLib.SQLTime();
newDoc.createdDate = defaultStr(newDoc.createdDate,date);
newDoc.createdHour = defaultStr(newDoc.createdHour,time);
if(options.updatedDate !== false || !isNonNullString(newDoc.updatedDate)){
newDoc.updatedDate = date;
}
if(options.updatedHour !== false || !isNonNullString(newDoc.updatedHour)){
newDoc.updatedHour = time;
}
/**** dans le document, est enregistré l'id du premier device l'ayant enregistré */
if(isNonNullString(APP.DEVICE.uuid)){
newDoc.uuid = defaultStr(newDoc.uuid,APP.DEVICE.uuid);
}
return newDoc;
}
const upsertVar = {};
/**
insert ou met à niveau la données dans la bd :
@param docId : string : null où l'id du document à mettre à jour
@param diffFun, function : la fonction de rappel permettant de retourner la différence avec le contenu du document trouvé
@param cb : function : la fonction de rappell appelée lorsque l'opération s'est déroulée avec succès
@param options : options de la base de données tel que recommandées par pouchdb
- si options est une fonction alors options joue le rôle de fonction de rappel
-si docId est une chaine de caractère, alors diffFunction sera obligatoire et appelée pour modifier la donnée trouvée
varientes :
docId,diffFunction,callback,pouchdbOptions
docId,diffFunction,callback,callbackError
docId,
upsert(_id,function(doc,docId){
return {docUpdate1,docUpdate2,}
}).then(function(){})
*/
upsertVar.upsert = function upsert(docId, diffFun, cb,options) {
var db = this;
var callbackErr = x=>{};
if(!isFunction(diffFun) || !isNonNullString(docId)){
let msg = "Upsertvar : impossible d'insérer la valer en base car la fonction diffFunc du plugin upsertVar est non définit ou l'id de la données est invalide";
throw msg;
}
if(isObj(cb)){
options = isObj(options)? options : cb;
cb = null;
}
if(isFunction(options)){
callbackErr = options;
options = null;
}
if(!isObj(options)){
options = {};
}
cb = typeof cb =="function"? cb : x=>x;
return upsertInner(db, docId, diffFun,options).then(function (resp) {
cb(resp);
return resp;
},callbackErr);
};
PouchDB.plugin(upsertVar);
//end upsert plugin
/**** la fonction trimIdField a pour rôle, de retirer les id préfixés par le nom de la table sur la valeur
* passé en paramètre :
* @param : value, string, chaine de caractère sur laquelle éliminer le préfix
* @param : table, string, le nom de la table vers laquelle a été définit l'id
* exempl: : db.trimIDField ('THIRD_PARTIES/CLIENT','THIRD_PARTIES') => CLIENT
*/
export const trimIDField = function(value,table){
let prefix = defaultStr(table).toUpperCase().trim();
value = defaultStr(value);
let split = value.split("/");
if(split[0].toUpperCase().trim() == prefix ){
let v = '';
for(let i = 1; i < split.length; i++){
v+= (v? '/':'')+split[i];
}
return v;
}
return value;
}
if(typeof window !=='undefined' && window && !isFunction(window.trimIdField)){
Object.defineProperties(window,{
trimIdField : {value:trimIDField,override:false,writable:false},
trimIDField : {value:trimIDField,override:false,writable:false},
})
}
export const getIdField = function(value,table,successCb,errorCb){
if(isFunction(table)){
if(isFunction(successCb)){
errorCb = errorCb || successCb;
}
successCb = table;
table = undefined;
}
let prefix = defaultStr(table).toUpperCase().trim()
value = this.trimIdField(value,table);
if(isNonNullString(prefix)){
value = prefix.rtrim("/")+"/"+value;
}
return new Promise((resolve,reject)=>{
this.get(value).then((v)=>{
if(isFunction(successCb)){
successCb(v);
} else resolve(v);
}).catch((e)=>{
if(isFunction(errorCb)){
errorCb(e);
} else {
reject(e);
}
})
})
};
PouchDB.plugin({
getIdField,
getIDField : getIdField,
getFieldId : getIdField,
trimIDField,
getName : function(){
return this? defaultStr(this.realName).ltrim(getDBNamePrefix()) : '';
},
archive : function(data){
let _dataArray = [];
if(isArray(data)){
data.map((dat,i)=>{
if(isDocUpdate(dat)){
dat.archived = 1;
_dataArray.push(dat);
}
})
} else if(isDocUpdate(data)){
data.archived = 1;
_dataArray.push(data);
}
showPreloader("archivage en cours...")
return this.bulkDocs(_dataArray).finally((r)=>{
hidePreloader();
return r;
});
},
trimIdField : trimIDField,
});
export const getTriggerDBTableName = (doc)=>{
doc = isObj(doc) && isDocUpdate(doc.doc)? doc.doc : doc;
return isObj(doc)? defaultStr(doc.table,doc.tableName,"DB_UNKNOW_TABLE") : undefined;
}
/***** Effectue le trigger en cas d'opération sur la base de données */
export const triggerDB = ({doc,db,isDelete,deleted}) =>{
if(isObj(doc) && isNonNullString(doc._id) && isNonNullString(doc._rev)){
let tableName = getTriggerDBTableName(doc);
const sDBName = structDataDBName.toLowerCase().trim();
if(isNonNullString(db.realName) && db.realName.trim().toLowerCase() == sDBName || (db?.infos?.code === sDBName) ){
tableName = triggerEventTableName(tableName);
}
const isDBLocked = db && isFunction(db.isLocked) && db.isLocked() ? true : false;
let action = actions.upsert(tableName);
const arg = {data:doc,isDBLocked,isTableLocked:isTableLocked(tableName) ? true : isDBLocked ? true : false,deleted,tableName,table:tableName,db,dbName:db.realName};
if(isDelete){
action = actions.onRemove(tableName);
}
APP.trigger(action,arg);
return true;
}
return false;
}
const initDB = ({db,pDBName,server,realName,localName,settings,isServer}) =>{
let {changes} = settings;
db.localName = localName;
db.isRemoteServer = db.isRemote = isServer ? true : false;
const DATABASES = POUCH_DATABASES.get();
const isDataFileManager = settings.isDataFileManager;
realName = realName.ltrim(getDBNamePrefix(isServer));
if(isNonNullString(realName) && !db.realName) {
Object.defineProperties(db,{
realName : {value:realName,override:false,writable:false}
});
}
const sDBName = sanitizeName(realName);
db.isDataFileManager = db.isManager = isDataFileManager;
if(!isObj(db.infos)){
Object.defineProperties(db,{
infos : {value : {
isDataFileManager,
isManager : isDataFileManager,
realName,
isServer,
type : defaultStr(settings.dataFileType,settings.type),
server,
isLocalServer : settings.isLocalServer,
serverUrl : server,
isRemote:isServer,
fullName : pDBName,
sanitizedName : sDBName,
}}
});
}
if(settings.willSkip){
Object.defineProperties(db,{
initialize : {
value : x => db.login(settings.username, settings.password),override:false,writable:false
}
});
db.initialize();
} else {
Object.defineProperties(db,{
initialize : {
value : x => Promise.resolve(),override:false ,writable:false
}
});
}
if(!db.isRemoteServer){
const {remove} = db;
if(DATABASES[sDBName]){
if(isValidDataFile(DATABASES[sDBName]?.infos)){
DATABASES[sDBName].retrievedFromCache = true;
return Promise.resolve(DATABASES[sDBName]);
}
}
const _remove = function({context,doc,force}){
if(!isBool(force)){
///par défaut, toutes les documents du fichier de données communes sont supprimées
force = isCommon(context.getName()) || isDataFileManager;
}
const sCB = (deleted,upserted)=>{
if(!isObj(doc) || !isNonNullString(doc._id)){
doc = deleted;
}
if(!isDataFileManager){
triggerDB({db,deleted,doc,isDelete:true});
}
return deleted;
}
if(force !== true && isObj(doc) && isNonNullString(doc._id)){
return context.upsert(doc._id,(newDoc)=>{
return {...newDoc,...doc,_deleted:true};
}).then((up)=>{
return sCB(up.newDoc,true);
});
}
return remove.call(context,doc).then(sCB);
}
db.remove = function(...args){
const doc = args[0];
const force = args[1];
const context = this;
if(isNonNullString(doc)){
return new Promise((resolve,reject)=>{
db.get(doc).then((_doc)=>{
args[0] = _doc;
return _remove({context,args,doc:_doc,force}).then((resolve)).catch((reject));
}).catch(reject);
});
}
return _remove({context,args,doc,force});
}
//db.setMaxListeners(20);
if(!isDataFileManager){
db.changesResult = db.changes(extendObj(true,{},{since: 'now',live: true,include_docs: true},changes))
.on('change', function(change) {
let tableName = getTriggerDBTableName(change);
if(!tableName) return;
tableName = tableName.toUpperCase().trim();
let dbName = pDBName.toLowerCase().trim();
db.lastChangeResult = change;
TRIGGER_CHANGES_DBS[dbName] = defaultObj(TRIGGER_CHANGES_DBS[dbName]);
TRIGGER_CHANGES_DBS[dbName][tableName] = defaultDecimal(TRIGGER_CHANGES_DBS[dbName][tableName]);
let lastChangeStart = TRIGGER_CHANGES_DBS[dbName][tableName];
if(isDecimal(lastChangeStart) && lastChangeStart > 0) return;
TRIGGER_CHANGES_DBS[dbName][tableName] = new Date().getTime();
setTimeout(()=>{
TRIGGER_CHANGES_DBS[dbName][tableName] = undefined;
if(isObj(db.lastChangeResult) && !db.lastChangeResult.deleted){//on évite les actions de suppression car supprimé par l'autre
let doc = db.lastChangeResult.doc;
triggerDB({db,doc,isDelete:false});
}
},10);
});
}
db.fullDBName = pDBName;
return new Promise((resolve)=>{
const p = !isDataFileManager && typeof db.getInfos ==='function' ? db.getInfos() : Promise.resolve({});
const cbSuccess = ()=>{
DATABASES[sDBName] = db;
resolve(db);
}
return isPromise(p)? p.catch((e=>e)).finally(cbSuccess) : cbSuccess();
});
}
return Promise.resolve(db);
}
/**** cré une instance pouchdb à partir des options passés en paramètre
* @param options {object} de la forme :
* {
* dbName| name : le nom de la bb,
* ... les autres options recommandés par pouchdb,
* ... changes : si l'on écoutera les évènements de la base de données, les options à passer
* }
* @return {PouchDB} instance de pouchdb Créé
*/
const newPouchDB = (options)=>{
options = defaultObj(options);
const settings = {...pouchdbRest,auto_compaction: true,size:10000000,revs_limit1:100,...options};
let pDBName = defaultStr(options.dbName,options.name);
if(!isValidUrl(pDBName)){
const localServerUrl = localServer.isEnabled && localServer.url || undefined;
if(isValidUrl(localServerUrl)){
options.server = localServerUrl;
options.isLocalServer = true;
options.username = localServer.username;
options.password = localServer.password;
}
}
options.dbName = pDBName;
const {realName} = options;
const parse = parseDBName(options);
delete options.name; delete options.dbName;
let {server} = parse;
const isServer = isValidUrl(server);
pDBName = sanitizeDBName(parse.dbName,isServer);
let localDBName = pDBName;
if(isServer) {
pDBName = urlJoin(server,pDBName).rtrim("/");
delete settings.adapter;
let {username,password} = settings;
if(isNonNullString(username) || isNonNullString(password)){
settings.auth = {...defaultObj(settings.auth),username,password};
}
} else {
pDBName = localDBName
}
const willSkip = isValidUrl(pDBName) && isNonNullString(settings.username) && isNonNullString(settings.password);
let args = {settings,pDBName,localName:localDBName};
if(!isServer && isElectron()){
pDBName = pDBName.rtrim(".db")+".db";
}
if(willSkip){
const {protocol,pathname,host} = parseURI(pDBName);
if(protocol && pathname && host){
pDBName = `${protocol}//${settings.username}:${settings.password}@${host.ltrim("/").rtrim("/")}/${pathname.ltrim("/")}`;
}
}
args.db = new PouchDB(pDBName, settings);
args.isServer = isServer;
args.realName = realName;
args.server = server;
args.isDataFileManager = options.isDataFileManager || isDataFileDBName(pDBName);
return initDB(args);
}
const resolveDB = (dbName,callback,options,error)=>{
const DATABASES = POUCH_DATABASES.get();
options = defaultObj(options);
let {force} = options;
const realName = dbName;
if(!isNonNullString(dbName)){
console.warn(dbName,' The dbName must be a non null string');
if(isFunction(error)){
return error({
msg : 'The dbName must be a non null string'
});
}
return Promise.reject({
msg : 'The dbName must be a non null string'
});
}
let db;
if(options && isValidUrl(options.server) || isValidUrl(dbName)){
force = true;
}
dbName = sanitizeName(dbName);
if(force !== true && DATABASES[dbName] != null && isFunction(DATABASES[dbName].upsert) && isValidDataFile(DATABASES[dbName].infos)){
db = DATABASES[dbName];
return new Promise((resolve,reject)=>{
db.info().then(() => {
if(typeof callback === 'function'){
callback({db,dbName});
}
resolve({db,dbName,realName});
})
.catch(e => {
resolveDBWithPromise(dbName,(args)=>{
if(isFunction(callback)){
callback(args);
}
resolve(args);
},realName);
});
})
}
return resolveDBWithPromise(dbName,callback,realName,options);
}
const resolveDBWithPromise = (dbName,callback,realName,options) =>{
options = defaultObj(options);
return newPouchDB({...options,dbName,realName}).then((db)=>{
db.NAME = db.Name = dbName;
const DATABASES = POUCH_DATABASES.get()
if(!db.isRemoteServer){
DATABASES[dbName] = db;
}
if(typeof callback === 'function'){
callback({db,dbName,realName});
}
return ({db,dbName,realName});
});
}
/**** return database options
* @param {string|object} name of database, or name with additional options
* @param {object} options to pass to pouchdb instanciation
* @return {object} sanitized dbOptions
*/
export const getDBOptions = (dbName,opts)=>{
const dbOpts = isObj(dbName)? dbName : isNonNullString(dbName)? {dbName} : {};
opts = extendObj({},dbOpts,opts);
const dataFileType = defaultStr(opts.dataFileType,opts.type);
dbOpts.dbName = dbOpts.name = getDBName(defaultStr(dbOpts.dbName,dbOpts.name).trim(),dataFileType);
dbOpts.dataFileType = dataFileType;
dbOpts.callback = defaultFunc(dbOpts.callback,dbOpts.success);
return dbOpts;
}
/*** retourne la base de données
@param : dbName : le nom de la bd
@param : options : les options supplémentaires à appliquer
ou encoure : @param : {
name : le nom de la bd
callback || success : la fonction de rappel en cas de succès
error || la fonction de rappel en cas d'erreur
adapter : le nom de l'adapter,
isDataFileManager : //si c'est le gestion de base de données des sources de données
...rest
}
*/
export default function getDBFunction (sDBName,opts){
const {dbName,callback,success,error,...options} = getDBOptions(sDBName,opts);
if(!isNonNullString(dbName)){
return Promise.reject({status:false,msg:'you must specify a database name that you want to retrieve'})
}
return resolveDB(dbName,callback,options,error)
}
export {getDBFunction as getDB};