apxtrib/api/models/Odmdb.js
2023-11-19 16:34:37 +01:00

680 lines
22 KiB
JavaScript

const glob = require("glob");
const path = require("path");
const fs = require("fs-extra");
const dayjs = require("dayjs");
const axios = require("axios");
const conf = require(`${process.env.dirtown}/conf.json`);
const Checkjson = require(`./Checkjson.js`);
/**
* This manage Objects for indexing, searching, checking and act to CRUD
* @objectPathName = objectpath/objectname
* objectpath/objectname/conf.json
* /idx/all_key1.json = {key1value:{object}}
* lst_key1.json = [key1valA,key1val2,...]
* key2_key1.json = {key2value:[key1value]}
* all_key1_filtername = {key1value:{object}}
* /itm/apxidvalue.json
* in conf.json:
* {
* "name": "object name ex:'nations'",
* "schema": "relativ schema from dirapi dirtown ex:'adminapi/schema/nations.json'"",
* "lastupdateschema": 0, time stamp last schema update
* "lastupdatedata":0 time stamp last itm update
* }
*
* Specifics key in schema to apXtrib:
* apxid : the field value to use to store item
* apxuniquekey : list of field that has to be unique you cannot have 2 itm with same key value
* apxidx : list of index file /idx/
* { "name":"lst_fieldA", "keyval": "alias" }, => lst_fieldA.json = [fieldAvalue1,...]
{ "name":"all_fieldA", "keyval": "fieldA" }, => all_fieldA.json =
if fieldA in apxuniquekey = {fieldAvalue1:{object}}
not in apxuniquekey = {fieldAvalue1:[{object}]}
{ "name":"word_fieldA", "keyval": "fieldA", "objkey": ["fieldB"]}, => word_fieldA.json
if fieldA in apxuniquekey = {fieldAvalue1:fieldBvalue,}
if fieldA not in uniquekey = {fieldAvalue1: [fieldBv1,fieldBv2,]}
{ "name":"word_fieldA", "keyval": "fieldA", "objkey": ["fieldB","fieldC"]}, => word_fieldA.json
if fieldA in apxuniquekey = {fieldAvalue1:{fieldB:val,fieldC:val},}
if fieldA not in uniquekey = {fieldAvalue1: [{fieldB:val,fieldC:val},]}
* apxaccessrights : list of profil with CRUD accesrights
**/
const Odmdb = {};
/**
* @api syncObject
* @param {string} url to an existing object conf (/objectname/conf.json)
* @param {timestamp} timestamp
* 0 => rebuild local object from all_{idapx}.json
* >0 => update itm and idx search by datetime
* @param
*/
Odmdb.syncObject = () => {};
/**
* @api createObject: create a space to host object
*
* @source {string} "new", url,
* @schemapath {string} path to create or replace a schema ${schemaPath}/schema/
* @objectPath {string} path where object are store
* @objectName {string} name of the object
* @schema {object} the json schema for this object
* @lgjson {object} the json file for a specific language
* @lg {string} the 2 letters language
*
* Setup a new object localy =>
* source
*
* - from scratch =>
* Create
* - from a synchronization
* Download from source all_{apxid}.json
*
*
*/
Odmdb.createObject = (
source,
schemaPath,
objectPath,
objectName,
schema,
lgjson,
lg
) => {
if (!fs.existsSync(schemaPath)) {
return {
status: 404,
ref: "Odmdb",
info: "pathnamedoesnotexist",
moreinfo: { fullpath: schemaPath },
};
}
if (!fs.existsSync(objectPath)) {
return {
status: 404,
ref: "Odmdb",
info: "pathnamedoesnotexist",
moreinfo: { fullpath: objectPath },
};
}
// store schema file if not empty undefined or {}
if (
schema &&
!(Object.keys(schema).length === 0 && schema.constructor === Object)
) {
fs.outputJSONSync(`${schemaPath}/schema/${objectName}.json`, schema, {
spaces: 2,
});
}
if (
lgjson &&
lg &&
!(Object.keys(lgjson).length === 0 && lgjson.constructor === Object)
) {
fs.outputJSONSync(`${schemaPath}/lg/${objectName}_${lg}.json`, lgjson, {
spaces: 2,
});
}
//create environnement object with the new schema config
if (!fs.existsSync(`${objectPath}/${objectName}`)) {
fs.outputJsonSync(
`${objectPath}/${objectName}/idx/confjson`,
{ schema: `${schemaPath}/schema/${objectName}.json` },
{ spaces: 2 }
);
}
return { status: 200 };
};
/**
* Update an object
* @param {string} objectPathname
* @param {object} meta update request
* lg:
* lgobj: object to store in /lg/objectname_lg.json
* schema: an update schema
* @return {status, ref:"Odmdb", msg:"", data}
*
* Create a tmp object env and check existing object to identify any issues
* If all is fine then apply change by replacing
*/
Odmdb.updateObject = (objectPathname, meta) => {};
/**
* Get a schema from objectPathname
*
* todo only local schema => plan a sync each 10minutes
* @schemaPath local path adminapi/schema/objectName.json or /tribename/schema/objectName
* @validschema boolean if necessary to check schema or not mainly use when change schema
* @return {status:200,data:{conf:"schemaconf",schema:"schemacontent"} }
*/
Odmdb.Schema = (objectPathname, validschema) => {
const confschema = fs.readJsonSync(`${objectPathname}/conf.json`);
let schemaPath = confschema.schema;
const res = {
status: 200,
ref: "Odmdb",
msg: "getschema",
data: { conf: confschema },
};
if (schemaPath.slice(-5) != ".json") schemaPath += ".json";
if (schemaPath.substring(0, 4) == "http") {
// lance requete http pour recuperer le schema avec un await axios
} else {
if (schemaPath.substring(0, 9) == "adminapi/") {
schemaPath = `${conf.dirapi}/${schemaPath}`;
} else {
schemaPath = `${conf.dirtown}/tribes/${schemaPath}`;
}
if (!fs.existsSync(schemaPath)) {
return {
status: 404,
ref: "Odmdb",
msg: "schemanotfound",
data: { schemaPath, schema: {} },
};
}
res.data.schema = fs.readJsonSync(schemaPath);
if (!res.data.schema.apxid) {
return {
status: 406,
ref: "Odmdb",
msg: "missingprimarykey",
data: {},
};
}
if (res.data.schema.apxidx) {
//add apxidx to apxuniquekey in case not
if (!res.data.schema.apxuniquekey.includes(res.data.schema.apxid)) {
res.data.schema.apxuniquekey.push(res.data.schema.apxid);
}
res.data.schema.apxidx.forEach((idx) => {
if (
idx.objkey &&
!res.data.schema.apxuniquekey.includes(idx.keyval) &&
!idx.objkey.includes(res.data.schema.apxid)
) {
return {
status: 406,
ref: "Odmdb",
msg: "unconsistencyapxidx",
data: {
name: idx.name,
keyval: idx.keyval,
objkey: idx.objkey,
apxid: res.data.schema.apxid,
},
};
}
});
}
if (validschema || 1 == 1) {
// return {status:200, ref, msg} or {status!:200,multimsg:[{ref,msg;data}]}
const check = Checkjson.schema.validation(res.data.schema);
if (check.status != 200) {
res.multimsg = check.multimsg;
res.status = check.status;
}
}
}
return res;
};
Odmdb.search = (objectPath, objectName, search) => {
/*
@search= {
txt: string,
algo: match | pattern | fuzzy
fieldstring:[list of field],
indexfilter:{index1:[val1,val2 | ] }
}
Return data:[uuids]
example: search exact match hill in townId
heavy search={txt:"hill",algo:"match",fieldstring:"toxnId"}
light search={txt:"hill", algo:"match", indexfilter:{"key":"townId","value":[]}}
light search={txt:"hill", algo:"match", indexfilter:{"key":"nationId","value":"ants"}}
*/
const schema = Odmdb.schema(objectPath, objectName);
if (schema.status != 200) return schema;
};
/**
* To get an array of item (itm) per primarykey with a list of field
* Object are store in objectPath/objectName/conf.json contain
*
* @objectPathname where object are store (where /object/conf.json indicate where the schema is)
* @uuidprimarykeyList list of uuid requested
* @role {xalias,xprofiles} allow to get accessright come from header
* @propertiesfilter (optionnal) key to return for each object (if undefined then return all)
* @Return {status:200; data:{uuid:{data filter by @propertiesfilter},uuid:"notfound"}}
*/
Odmdb.reads = (objectPathname, apxidlist, role, propertiesfilter) => {
const res = { status: 200, data: {} };
const getschema = Odmdb.Schema(objectPathname, true);
if (getschema.status != 200) return getschema;
// Test if readable at least if owner
role.xprofils.push("owner");
const accessright = (Odmdb.accessright =
(getschema.data.schema.apxaccessright, role.xprofils));
if (!accessright.R) {
return {
status: 403,
ref: "Odmdb",
msg: "accessforbidden",
data: { crud: "R", accessright },
};
}
apxidlist.forEach((id) => {
if (fs.existsSync(`${objectPathname}/itm/${id}.json`)) {
const objectdata = fs.readJsonSync(`${objectPathname}/itm/${id}.json`);
if (objectdata.owner && objectdata.owner == role.xalias) {
if (!role.xprofils.includes("owner")) role.xprofils.push("owner");
} else {
if (role.xprofils.includes("owner"))
role.xprofils = role.xprofils.filter((e) => e !== "owner");
}
const accessright = Odmdb.accessright(
getschema.data.schema.apxaccessright,
role
);
if (!accessright.R) {
res.data[id] = "forbiden";
} else {
let newpropertiesfilter = Object.keys(objectdata);
if (accessright.R.length > 0) {
const setaccess = new Set(accessright.R);
if (!propertiesfilter) propertiesfilter = Object.keys(objectdata);
newpropertiesfilter = propertiesfilter.filter((f) =>
setaccess.has(f)
);
}
const objinfo = {};
newpropertiesfilter.forEach((k) => {
if (objectdata[k]) objinfo[k] = objectdata[k];
});
res.data[id] = objinfo;
}
} else {
res.data[id] = "notfound";
}
});
return res;
};
/**
* Convert profils in accessright
* @param {*} apxaccessright from schema object {profilname:{R}}
* @param {*} role {xprofils,xalias} accessible after isAuthenticated
* @returns access right to C create if present, to read (properties list or all if empty), to Update properties list or all if empty, D elete
* example: {"C":[],"R":[properties list],"U":[properties ist],"D":[]}
*/
Odmdb.accessright = (apxaccessrights, role) => {
const accessright = {};
role.xprofils.forEach((p) => {
if (apxaccessrights[p]) {
Object.keys(apxaccessrights[p]).forEach((act) => {
if (!accessright[act]) {
accessright[act] = apxaccessrights[p][act];
} else {
accessright[act] = [
...new Set([...accessright[act], ...apxaccessrights[p][act]]),
];
}
});
}
});
return accessright;
};
/**
* CUD a data itm into objectPathname if checkJson is valid
* and update idx
* idx is upto date for unique properties but not for list
* @param {string} objectpathname folder name where object are stored
* @param {object} itm an object respecting the checkJson schema in objectPathname/conf.json
* @param {string} crud: C reate U pdate D elete
* @param {array} role {xprofils,xalias} xprofils list of profils like anonymous,pagans, person owner is deuce if object properties owner is alias
* */
Odmdb.cud = (objectPathname, crud, itm, role) => {
const getschema = Odmdb.Schema(objectPathname, true);
if (getschema.status != 200) return getschema;
if (!itm[getschema.data.schema.apxid]) {
return {
status: 406,
ref: "Odmdb",
msg: "apxidmissing",
data: { missingkey: getschema.data.schema.apxid },
};
}
const pathindex=`${objectPathname}/idx/lst_${getschema.data.schema.apxid}.json`;
if (!fs.existsSync(pathindex)){
fs.outputJSONSync(pathindex,[])
fs.ensureDirSync(`${objectPathname}/itm/`);
}
const existid = fs
.readJSONSync(pathindex)
.includes(itm[getschema.data.schema.apxid]);
if (existid && crud == "C") {
return {
status: 406,
ref: "Odmdb",
msg: "alreadyexist",
data: {
objectname: path.basename(objectPathname),
key: getschema.data.schema.apxid,
val: itm[getschema.data.schema.apxid],
},
};
}
if (!existid && ["U", "D"].includes(crud)) {
return {
status: 406,
ref: "Odmdb",
msg: "doesnotexist",
data: {
objectname: path.basename(objectPathname),
key: getschema.data.schema.apxid,
val: itm[getschema.data.schema.apxid],
},
};
}
const itmold = existid
? fs.readJSONSync(
`${objectPathname}/itm/${itm[getschema.data.schema.apxid]}.json`
)
: {};
if (existid && itmold.owner && itmold.owner == role.xalias) {
role.xprofils.push("owner");
} else {
// set owner cause this is a Create
itm.owner = role.xalias;
}
//get accessright {C:[],R:[],U:[],D:[]} if exist means authorize, if array contain properties (for R and U) right is only allowed on properties
const accessright = Odmdb.accessright(
getschema.data.schema.apxaccessrights,
role
);
console.log("accessright", accessright);
if (
(crud == "C" && !accessright.C) ||
(crud == "D" && !accessright.D) ||
(crud == "U" && !accessright.U)
) {
return {
status: 403,
ref: "Odmdb",
msg: "accessforbidden",
data: { crud, accessright },
};
}
//delete or save
if (crud == "D") {
itmold["dt_delete"] = dayjs();
fs.outputJSONSync(
`${objectPathname}/delitm/${itmold[getschema.data.schema.apxid]}.json`,
itmold
);
fs.rmSync(
`${objectPathname}/itm/${itmold[getschema.data.schema.apxid]}.json`
);
} else {
// if Create Update erase old version
let itmtostore = itm;
if (crud == "U" && accessright.U.length > 0) {
itmtostore = itmold;
accessright.U.forEach((p) => {
itmtostore[p] = itm[p];
});
itmtostore.dt_update = dayjs();
}
if (crud == "C") itmtostore.dt_create = dayjs();
// check consistency of datatostore
const chkdata = Checkjson.schema.data(
getschema.data.schema,
itmtostore,
false
);
if (chkdata.status != 200) return chkdata;
if (!getschema.data.schema.apxuniquekey)
getschema.data.schema.apxuniquekey = [];
fs.outputJSONSync(
`${objectPathname}/itm/${chkdata.data.apxid}.json`,
chkdata.data.itm
);
}
console.log("getschema", getschema);
//update idx
Odmdb.idxfromitm(
objectPathname,
crud,
itm,
itmold,
[],
getschema.data.schema
);
getschema.data.conf.lastupdatedata = dayjs();
fs.outputJSONSync(`${objectPathname}/conf.json`, getschema.data.conf);
return {
status: 200,
ref: "Odmdb",
msg: "cudsuccessfull",
data: { itm: chkdata.data.itm },
};
};
/**
* create/update idx from itm(s)
*
* @param {string} objectPathname
* @param {object} itm item to Create or to Update or {} if crud == I or crud == D
* @param {object} itmold (current item) if crud == U or D to get previous itm before change or {} if crud==I or C
* @param {letter} crud CUDI C add, U update, D delete I reindex
* @param {array} idx if specific request to rebuild list of idx only if [] then use schema one
* @param {object} schema if empty it use schema from Odmdb.Schema().data.schema
*
* example create alias 12 name fred:
* Odmdb.idxfromitm('.../tribes/ndda/persons',"C",{alias:'12',name:"fred"},{},[], {person schema})
* example update alias 12 in name freddy:
* Odmdb.idxfromitm('.../tribes/ndda/persons',"U",{alias:'12',name:"freddy"},{alias:'12',name:"fred"},[], {person schema})
* example delete alias 12:
* Odmdb.idxfromitm('.../tribes/ndda/persons',"D",{},{alias:'12',name:"fred"},[], {person schema})
* example to rebuild all index from scratch
* Odmdb.idxfromitm('.../tribes/ndda/persons',"I",{},{},[], {person schema})
* example to rebuild only publickey_alias index from scratch
* Odmdb.idxfromitm('.../tribes/ndda/pagans',"I",{},{},[{ name:"publickey_alias",keyval:"publickey",objkey:["alias"]}], {pagans schema})
*
*/
Odmdb.idxfromitm = (objectPathname, crud, itm, itmold, idxs = [], schema) => {
console.log(`idxfromitem for ${objectPathname} action:${crud}`);
if (!schema || !schema.apxid) {
const getschema = Odmdb.Schema(objectPathname, true);
if (getschema.status != 200) return getschema;
schema = getschema.data.schema;
}
console.log(schema.apxuniquekey);
const itms = crud == "I" ? glob.sync(`${objectPathname}/itm/*.json`) : [itm];
console.log(itms);
if (crud == "I") {
//reinit all idx
idxs.forEach((idx) => {
fs.remove(`${objectPathname}/idx/${idx.name}.json`);
});
}
let idxtoreindex = []; //store index that has to be reprocessto get full context
idxs = idxs.length == 0 ? schema.apxidx : idxs; // get all index if none
itms.forEach((i) => {
if (crud == "I") {
itm = fs.readJSONSync(i);
}
//console.log(itm);
idxs.forEach((idx) => {
const keyvalisunique = schema.apxuniquekey.includes(idx.keyval); // check if keyval is unique mean store as an object (or string) else store as an array
const idxsrc = `${objectPathname}/idx/${idx.name}.json`;
const idxinit = idx.name.substring(0, 4) == "lst_" ? [] : {}; // select type of idx (array or object)
let idxfile = !fs.existsSync(idxsrc) ? idxinit : fs.readJSONSync(idxsrc);
if (idx.name.substring(0, 4) == "lst_") {
if (["D", "U"].includes(crud)) {
if (keyvalisunique) {
idxfile = idxfile.filter((e) => e !== itmold[idx.keyval]);
} else {
idxtoreindex.push(idx); //@todo
}
}
console.log(idx.keyval);
console.log(itm[idx.keyval]);
if (
["C", "U", "I"].includes(crud) &&
!idxfile.includes(itm[idx.keyval])
) {
idxfile.push(itm[idx.keyval]);
}
} else {
if (!idx.objkey) {
//mean all properties
idx.objkey = Object.keys(schema.properties);
}
if (keyvalisunique && idx.objkey.length == 1) {
if (["D", "U"].includes(crud)) {
delete idxfile[itmold[idx.keyval]];
} else {
idxfile[itm[idx.keyval]] = itm[idx.objkey[0]];
}
}
if (keyvalisunique && idx.objkey.length > 1) {
if (["D", "U"].includes(crud)) {
delete idxfile[itmold[idx.keyval]];
} else {
const itmfilter = {};
idx.objkey.forEach((i) => {
if (itm[i]) itmfilter[i] = itm[i];
});
idxfile[itm[idx.keyval]] = itmfilter;
}
}
if (!keyvalisunique && idx.objkey.length == 1) {
if (
["D", "U"].includes(crud) &&
idxfile[itmold[idx.keyval]].IndexOf(itmold[idx.objkey[0]]) > -1
) {
// U because need to remove previous value before adding it
idxfile[itmold[idx.keyval]].splice(
idxfile[itmold[idx.keyval]].IndexOf(itmold[idx.objkey[0]]),
1
);
}
if (["C", "U", "I"].includes(crud)) {
if (!idxfile[itm[idx.keyval]]) idxfile[itm[idx.keyval]] = [];
if (!idxfile[itm[idx.keyval]].includes(itm[idx.objkey[0]])) {
idxfile[itm[idx.keyval]].push(itm[idx.objkey[0]]);
}
}
}
if (!keyvalisunique && idx.objkey.length > 1) {
if (["D", "U"].includes(crud) && idxfile[itmold[idx.keyval]]) {
// U because need to remove previous value before adding it
let arrayofit = [];
idxfile[itmold[idx.keyval]].forEach((it) => {
if (it[schema.apxid] != itm[schema.apxid]) arrayofit.push(it);
});
idxfile[itmold[idx.keyval]] = arrayofit;
}
if (["C", "U", "I"].includes(crud)) {
const itmfilter = {};
idx.objkey.forEach((i) => {
if (itm[i]) itmfilter[i] = itm[i];
});
if (!idxfile[itm[idx.keyval]]) idxfile[itm[idx.keyval]] = [];
idxfile[itm[idx.keyval]].push(itmfilter);
}
}
}
fs.outputJSONSync(idxsrc, idxfile);
});
});
if (crud != "I") {
//update lastupdatedata to inform something change
const confschema = fs.readJSONSync(`${objectPathname}/conf.json`);
confschema.lastupdatedata = dayjs();
fs.outputJSONSync(`${objectPathname}/conf.json`, getschema.data.conf);
}
return { status: 200, ref: "Odmdb", msg: "successreindex", data: {} };
};
Odmdb.updatefromidxall = (objectname, idxname, data, lastupdate) => {
/**
* Update all itm of objectname from index idx/idxname with data
* if itm exist in local and not in data then /ojectname/conf.json.lastupdate = now
* not then /ojectname/conf.json.lastupdate = lastupdate
* this way mean next time server A want to refresh from B its lastupdate < than on A
*/
let conflastupdate = 0;
let localidx = {};
if (
fs.existsSync(`${conf.dirapi}/nationchains/${objectname}/idx/${idxname}`)
) {
localidx = fs.readJsonSync(
`${conf.dirapi}/nationchains/${objectname}/idx/${idxname}`
);
}
Object.keys(data).forEach((id) => {
if (localidx[id]) {
if (
localidx[id].dt_update &&
data[id].dt_update &&
localidx[id].dt_update > data[id].dt_update
) {
// means local information is fresher than the one in data for replacement
// .toISIString ex: 2019-01-25T02:00:00.000Z'
conflastupdate = dayjs().toISOString();
} else {
// replace itm with data
localidx[id] = data[id];
fs.outputJsonSync(
`${conf.dirapi}/nationchains/${objectname}/itm/${id}.json`,
data[id]
);
}
} else {
// add itm
localidx[id] = data[id];
fs.outputJsonSync(
`${conf.dirapi}/nationchains/${objectname}/itm/${id}.json`,
data[id]
);
}
});
//check if it miss id in fresher update means conf.lastupdate will be now to indicate
Object.keys(localidx).forEach((id) => {
if (!data[id]) {
conflastupdate = dayjs();
}
});
// update the object files
if (conflastupdate == 0) conflastupdate = lastupdate;
fs.outputJSONSync(
`${conf.dirapi}/nationchains/${objectname}/idx/${idxname}`,
localidx
);
const objconf = fs.readJsonSync(
`${conf.dirapi}/nationchains/${objectname}/conf.json`
);
objconf.lastupdate = conflastupdate;
fs.outputJsonSync(
`${conf.dirapi}/nationchains/${objectname}/conf.json`,
objconf
);
return {
status: 200,
ref: "Odmdb.js",
info: "Successfullupdate",
data: { objectname, idxname, lastupdate },
};
};
module.exports = Odmdb;