UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

322 lines (299 loc) 17.1 kB
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();