json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
322 lines (299 loc) • 17.1 kB
JavaScript
var fs = require('fs');
var serverPath = '../';
function Schemas(){
var self = this;
this.schema = {};
this.schemaList =[];
this.raw_schemas = {};
this.summary = {};
this.summaryGeneratedAt = null;
this.updateDelay = 1200;
//this.core = {};
var internalSchemasDir = __dirname+'/../schemas';
//console.log('internal schema dir = '+internalSchemasDir);
JOE.schemaDir = schemaDir = JOE.appDir+'/'+JOE.webconfig.schemaDir+'/';
this.load = function(s){
//console.log('[schema] loading '+s);
var schema,raw_schema;
var origin = 'instance';
if(s.indexOf('.js') == -1){s+= '.js';}
try{
var schemapath = JOE.schemaDir + s;
fs.accessSync(schemapath, fs.F_OK);
}catch(e){
var schemapath = serverPath+'schemas/' + s;
origin = 'core';
}
try{
try{
delete require.cache[require.resolve(schemapath)];
}catch(e){
console.log('[schema][error] '+s+' not deleted');
}
schema = require(schemapath);
try { schema.__origin = origin; } catch(_e) {}
}catch(e){
schema = {error:'schema '+s+' not found'};
console.log(e,schema);
}
/*raw_schema = self.raw_schemas[s.replace('.js','')] = $c.merge({},schema);
if(raw_schema.events){
raw_schema.events = $c.merge({},raw_schema.events);
}*/
raw_schema = self.raw_schemas[s.replace('.js','')] = schema.duplicate(true);
JOE.Utils.stringFunctions(schema);
let schemaname = s.replace('.js','')
self.schema[schemaname] = schema;
self.schemaList.push(schemaname);
return schema;
}
this.updateTimeout;
// Build normalized schema summaries for agents and tooling
this.buildSummary = function(){
try{
var summaries = {};
var MAX_META_LEN = 160;
function truncMeta(v){
if (typeof v !== 'string') return v;
return (v.length > MAX_META_LEN) ? (v.slice(0, MAX_META_LEN-1) + '…') : v;
}
// First pass: collect outbound relationships and field basics
Object.keys(self.schema||{}).forEach(function(name){
var def = self.schema[name] || {};
var fieldDefs = [];
var outbound = [];
var searchable = Array.isArray(def.searchables) ? def.searchables.slice() : [];
var allowedSorts = [];
var defaultSort = null;
// Sorters
if (Array.isArray(def.sorter)){
def.sorter.forEach(function(s){
var fieldName = null, dir = 'asc';
if (typeof s === 'string'){
fieldName = s.replace(/^!/, function(){ dir='desc'; return ''; });
} else if (s && typeof s === 'object' && typeof s.field === 'string'){
fieldName = s.field.replace(/^!/, function(){ dir='desc'; return ''; });
}
if (fieldName){
allowedSorts.push(fieldName);
if (!defaultSort) { defaultSort = { field: fieldName, dir: dir }; }
}
});
}
if (!defaultSort){ defaultSort = { field: 'joeUpdated', dir: 'desc' }; }
// Fields (robust extraction)
var rawFields = [];
try{
if (Array.isArray(def.fields)) { rawFields = def.fields; }
else if (typeof def.fields === 'function') { rawFields = def.fields.call(def) || []; }
}catch(_e){ rawFields = []; }
function inferRef(fieldObj){
var tSchema = null, isRef = false, isArr = false;
var t = fieldObj.type || 'unknown';
if (t === 'objectList' || t === 'array') { isArr = true; }
if (t === 'select'){
if (typeof fieldObj.schema === 'string'){ isRef = true; tSchema = fieldObj.schema; }
else if (typeof fieldObj.values === 'string'){ isRef = true; tSchema = fieldObj.values; }
else if (typeof fieldObj.goto === 'string'){ isRef = true; tSchema = fieldObj.goto; }
}
// Heuristics by name
var n = fieldObj.name || '';
if (!isRef && typeof n === 'string'){
if (n === 'status'){ isRef = true; tSchema = 'status'; }
else if (n === 'project'){ isRef = true; tSchema = 'project'; }
else if (n === 'assignee'){ isRef = true; tSchema = 'user'; }
else if (n === 'members'){ isRef = true; tSchema = 'user'; isArr = true; }
else if (n === 'tags'){ isRef = true; tSchema = 'tag'; isArr = true; }
else if (/_id$/.test(n)){ isRef = true; tSchema = n.replace(/_id$/, ''); }
else if (/Id$/.test(n)){ isRef = true; tSchema = n.replace(/Id$/, ''); }
}
return { isReference: !!isRef, targetSchema: tSchema, isArray: !!isArr };
}
function pushFieldEntry(entry){
if (!entry) return;
if (typeof entry === 'string'){
// string shorthand
var refGuess = inferRef({ name: entry, type: 'string' });
fieldDefs.push({ name: entry, type: 'string', isArray: false, isReference: refGuess.isReference, targetSchema: refGuess.targetSchema });
if (refGuess.isReference && refGuess.targetSchema){ outbound.push({ field: entry, targetSchema: refGuess.targetSchema }); }
return;
}
if (typeof entry === 'object'){
// skip layout markers
if (entry.section_start || entry.section_end || entry.sidebar_start || entry.sidebar_end) { return; }
// Treat extend as a real field mapped to core definition with specs overlay
if (entry.extend){
try{
var fname = entry.extend;
var coreDef = (JOE && JOE.Fields && (JOE.Fields.core || JOE.Fields.fields) && (JOE.Fields.core[fname] || JOE.Fields.fields[fname])) || null;
var inferredType = (entry.specs && entry.specs.type) || (coreDef && coreDef.type) || 'string';
var ref2 = inferRef({ name: fname, type: inferredType });
var fobjE = { name: fname, type: inferredType, isArray: !!(coreDef && coreDef.isArray) || (inferredType==='objectList'), isReference: ref2.isReference, targetSchema: ref2.targetSchema };
if (entry.specs && entry.specs.display) { fobjE.display = entry.specs.display; }
else if (coreDef && coreDef.display) { fobjE.display = coreDef.display; }
if (entry.specs && entry.specs.comment) { fobjE.comment = truncMeta(entry.specs.comment); }
else if (coreDef && coreDef.comment) { fobjE.comment = truncMeta(coreDef.comment); }
if (entry.specs && entry.specs.tooltip) { fobjE.tooltip = truncMeta(entry.specs.tooltip); }
else if (coreDef && coreDef.tooltip) { fobjE.tooltip = truncMeta(coreDef.tooltip); }
fieldDefs.push(fobjE);
if (ref2.isReference && ref2.targetSchema){ outbound.push({ field: fname, targetSchema: ref2.targetSchema, cardinality: (fobjE.isArray ? 'many' : 'one') }); }
return;
}catch(_e){ /* fall through */ }
}
if (entry.name){
var t = entry.type || 'unknown';
var ref = inferRef(entry);
var fobj = { name: entry.name, type: t, isArray: !!ref.isArray || t === 'objectList', isReference: ref.isReference, targetSchema: ref.targetSchema };
if (entry.display) { fobj.display = entry.display; }
if (entry.comment) { fobj.comment = truncMeta(entry.comment); }
if (entry.tooltip) { fobj.tooltip = truncMeta(entry.tooltip); }
fieldDefs.push(fobj);
if (ref.isReference && ref.targetSchema){ outbound.push({ field: entry.name, targetSchema: ref.targetSchema, cardinality: (ref.isArray || t === 'objectList') ? 'many' : 'one' }); }
}
// If objectList has properties, we don't descend (embedded), keep top-level only
}
}
rawFields.forEach(pushFieldEntry);
// Ensure core timestamps/ids appear even if not declared explicitly
['_id','itemtype','joeUpdated','created'].forEach(function(coreName){
if (!fieldDefs.some(function(fd){ return fd.name === coreName; })){
fieldDefs.push({ name: coreName, type: coreName === '_id' ? 'string' : (coreName==='joeUpdated'||coreName==='created'?'date-time':'string') });
}
});
// Label field guess
var labelField = null;
['name','title','label'].some(function(c){
var has = fieldDefs.some(function(fd){ return fd && fd.name === c; });
if (has){ labelField = c; return true; }
return false;
});
// Ensure common sorts appear
['joeUpdated','created','name'].forEach(function(sf){ if (allowedSorts.indexOf(sf) === -1) { allowedSorts.push(sf); } });
var baseSummary = {
description: (def.summary && typeof def.summary.description === 'string') ? def.summary.description : '',
purpose: (def.summary && typeof def.summary.purpose === 'string') ? def.summary.purpose : '',
wip: !!def.wip,
source: (def.summary && typeof def.summary.source === 'string') ? def.summary.source : ((def.__origin === 'core') ? 'core' : 'instance'),
labelField: labelField || 'name',
defaultSort: defaultSort,
searchableFields: searchable,
allowedSorts: allowedSorts,
relationships: { outbound: outbound, inbound: { graphRef: 'server/relationships.graph.json' } },
fields: fieldDefs,
fieldCountBase: fieldDefs.length,
joeManagedFields: ['created','joeUpdated']
};
// Allow curated overrides from schema.summary
var finalSummary = baseSummary;
if (def.summary && typeof def.summary === 'object'){
// Shallow merge top-level props, but merge fields by name (do not replace entire list)
var curated = def.summary;
finalSummary = Object.assign({}, baseSummary, curated);
try{
var mergedFields = (baseSummary.fields||[]).slice();
var byName = {};
mergedFields.forEach(function(f, idx){ byName[f.name] = { idx: idx, field: f }; });
if (Array.isArray(curated.fields)){
curated.fields.forEach(function(cf){
if (!cf || !cf.name) return;
if (byName[cf.name]){
// overlay curated props onto base field
var existing = mergedFields[byName[cf.name].idx];
mergedFields[byName[cf.name].idx] = Object.assign({}, existing, cf);
} else {
mergedFields.push(cf);
}
});
}
finalSummary.fields = mergedFields;
}catch(_e){ finalSummary.fields = baseSummary.fields; }
}
// Enrich curated fields with display/comment/tooltip from base when missing
try{
var baseFieldByName = {};
(baseSummary.fields||[]).forEach(function(f){ baseFieldByName[f.name] = f; });
(finalSummary.fields||[]).forEach(function(f){
var bf = baseFieldByName[f.name];
if (!bf) return;
if (f.display == null && bf.display != null) f.display = bf.display;
if (f.comment == null && bf.comment != null) f.comment = truncMeta(bf.comment);
if (f.tooltip == null && bf.tooltip != null) f.tooltip = truncMeta(bf.tooltip);
if (f.comment) f.comment = truncMeta(f.comment);
if (f.tooltip) f.tooltip = truncMeta(f.tooltip);
});
}catch(_e){}
// Final counts
try{
finalSummary.fieldCount = Array.isArray(finalSummary.fields) ? finalSummary.fields.length : 0;
if (typeof finalSummary.fieldCountBase !== 'number'){
finalSummary.fieldCountBase = baseSummary.fieldCountBase || 0;
}
}catch(_e){}
summaries[name] = finalSummary;
});
// Inbound relationships will be maintained in a shared graph file; do not compute here
self.summary = summaries;
self.summaryGeneratedAt = new Date();
}catch(e){
console.log('[Schemas] buildSummary error:', e);
}
}
this.update = function(doItNow){
if(doItNow){
var lBM = new Benchmarker();
var schema_to_load = [];
//go through all the server files, then the schemadir files\
schema_to_load = schema_to_load.concat(fs.readdirSync(internalSchemasDir));
//schema_to_load = schema_to_load.concat(fs.readdirSync('server/schemas'));
schema_to_load = schema_to_load.concat(fs.readdirSync(JOE.schemaDir));
schema_to_load.map(function(sc){
self.load(sc);
})
// Build normalized summaries after load
try { self.buildSummary(); } catch(_e){}
logit(JOE.Utils.color('[schema] ','module')+schema_to_load.length+' updated in '+lBM.stop()+' secs');
}else{
clearTimeout(self.updateTimeout);
self.updateTimeout = setTimeout(self.update,self.updateDelay,true);
//logit(JOE.Utils.color('[schema] ','module')+'updating in '+ this.updateDelay/1000);
}
}
JOE.Utils.setupFileFolder(schemaDir,'schemas',self.update);
fs.watch(internalSchemasDir,function(){
self.update();
});
this.events = function(item,events,specs){
try{
var specs = specs || {};
var schemaprop = specs.schemaprop || "itemtype";
var schemaname = item[schemaprop];
var schema_def = self.raw_schemas[schemaname];
if(schema_def){
if(typeof events == "string"){
events = events.split(',');
}
events.map(function(event){
if(schema_def.events && schema_def.events[event]){
if(typeof schema_def.events[event] == "function"){
try{
schema_def.events[event](item,specs);
logit('[event] '+schemaname+' > '+event);
}catch(e){
console.log(JOE.Utils.color('[schema]event error: ','error')+event+':'+e);
}
}else{
logit('no event'+event+' for '+schemaname );
}
}
})
}else{
logit('schema "'+schemaname+'" not found');
}
}catch(e){
console.log(JOE.Utils.color('[schema] hook error: ','error')+events+':'+e);
}
}
this.update();
return self;
}
module.exports = new Schemas();