json-object-editor 0.10.509 → 0.10.521

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/_www/mcp-export.html +11 -4
  3. package/_www/mcp-nav.js +8 -4
  4. package/_www/mcp-prompt.html +96 -121
  5. package/_www/mcp-schemas.html +294 -0
  6. package/_www/mcp-test.html +86 -0
  7. package/docs/JOE_Master_Knowledge_Export.md +135 -0
  8. package/docs/joe_agent_custom_gpt_instructions_v_2.md +54 -0
  9. package/docs/joe_agent_spec_v_2.2.md +64 -0
  10. package/docs/schema_summary_guidelines.md +128 -0
  11. package/package.json +1 -1
  12. package/readme.md +525 -469
  13. package/server/modules/MCP.js +606 -405
  14. package/server/modules/Schemas.js +321 -111
  15. package/server/modules/Server.js +26 -15
  16. package/server/modules/Storage.js +9 -0
  17. package/server/relationships.graph.json +5 -0
  18. package/server/schemas/block.js +37 -0
  19. package/server/schemas/board.js +2 -1
  20. package/server/schemas/budget.js +28 -1
  21. package/server/schemas/event.js +42 -0
  22. package/server/schemas/financial_account.js +35 -0
  23. package/server/schemas/goal.js +30 -0
  24. package/server/schemas/group.js +31 -0
  25. package/server/schemas/include.js +28 -0
  26. package/server/schemas/ingredient.js +28 -0
  27. package/server/schemas/initiative.js +32 -0
  28. package/server/schemas/instance.js +31 -1
  29. package/server/schemas/layout.js +31 -0
  30. package/server/schemas/ledger.js +30 -0
  31. package/server/schemas/list.js +33 -0
  32. package/server/schemas/meal.js +30 -0
  33. package/server/schemas/note.js +30 -0
  34. package/server/schemas/notification.js +33 -1
  35. package/server/schemas/page.js +43 -0
  36. package/server/schemas/post.js +32 -0
  37. package/server/schemas/project.js +36 -0
  38. package/server/schemas/recipe.js +32 -0
  39. package/server/schemas/report.js +32 -0
  40. package/server/schemas/setting.js +22 -0
  41. package/server/schemas/site.js +30 -0
  42. package/server/schemas/status.js +33 -0
  43. package/server/schemas/tag.js +28 -1
  44. package/server/schemas/task.js +778 -737
  45. package/server/schemas/transaction.js +43 -0
  46. package/server/schemas/user.js +36 -1
  47. package/server/schemas/workflow.js +30 -1
@@ -1,112 +1,322 @@
1
- var fs = require('fs');
2
- var serverPath = '../';
3
- function Schemas(){
4
- var self = this;
5
- this.schema = {};
6
- this.schemaList =[];
7
- this.raw_schemas = {};
8
- this.updateDelay = 1200;
9
- //this.core = {};
10
- var internalSchemasDir = __dirname+'/../schemas';
11
- //console.log('internal schema dir = '+internalSchemasDir);
12
- JOE.schemaDir = schemaDir = JOE.appDir+'/'+JOE.webconfig.schemaDir+'/';
13
-
14
- this.load = function(s){
15
-
16
- //console.log('[schema] loading '+s);
17
- var schema,raw_schema;
18
- if(s.indexOf('.js') == -1){s+= '.js';}
19
- try{
20
- var schemapath = JOE.schemaDir + s;
21
- fs.accessSync(schemapath, fs.F_OK);
22
-
23
- }catch(e){
24
- var schemapath = serverPath+'schemas/' + s;
25
- }
26
- try{
27
- try{
28
- delete require.cache[require.resolve(schemapath)];
29
- }catch(e){
30
- console.log('[schema][error] '+s+' not deleted');
31
- }
32
- schema = require(schemapath);
33
-
34
- }catch(e){
35
- schema = {error:'schema '+s+' not found'};
36
- console.log(e,schema);
37
- }
38
-
39
- /*raw_schema = self.raw_schemas[s.replace('.js','')] = $c.merge({},schema);
40
- if(raw_schema.events){
41
- raw_schema.events = $c.merge({},raw_schema.events);
42
- }*/
43
- raw_schema = self.raw_schemas[s.replace('.js','')] = schema.duplicate(true);
44
- JOE.Utils.stringFunctions(schema);
45
- let schemaname = s.replace('.js','')
46
- self.schema[schemaname] = schema;
47
- self.schemaList.push(schemaname);
48
- return schema;
49
- }
50
- this.updateTimeout;
51
- this.update = function(doItNow){
52
- if(doItNow){
53
- var lBM = new Benchmarker();
54
- var schema_to_load = [];
55
- //go through all the server files, then the schemadir files\
56
- schema_to_load = schema_to_load.concat(fs.readdirSync(internalSchemasDir));
57
- //schema_to_load = schema_to_load.concat(fs.readdirSync('server/schemas'));
58
- schema_to_load = schema_to_load.concat(fs.readdirSync(JOE.schemaDir));
59
- schema_to_load.map(function(sc){
60
- self.load(sc);
61
- })
62
- logit(JOE.Utils.color('[schema] ','module')+schema_to_load.length+' updated in '+lBM.stop()+' secs');
63
- }else{
64
- clearTimeout(self.updateTimeout);
65
- self.updateTimeout = setTimeout(self.update,self.updateDelay,true);
66
- //logit(JOE.Utils.color('[schema] ','module')+'updating in '+ this.updateDelay/1000);
67
-
68
- }
69
- }
70
- JOE.Utils.setupFileFolder(schemaDir,'schemas',self.update);
71
- fs.watch(internalSchemasDir,function(){
72
- self.update();
73
- });
74
-
75
- this.events = function(item,events,specs){
76
- try{
77
- var specs = specs || {};
78
- var schemaprop = specs.schemaprop || "itemtype";
79
- var schemaname = item[schemaprop];
80
- var schema_def = self.raw_schemas[schemaname];
81
-
82
- if(schema_def){
83
-
84
- if(typeof events == "string"){
85
- events = events.split(',');
86
- }
87
- events.map(function(event){
88
- if(schema_def.events && schema_def.events[event]){
89
- if(typeof schema_def.events[event] == "function"){
90
- try{
91
-
92
- schema_def.events[event](item,specs);
93
- logit('[event] '+schemaname+' > '+event);
94
- }catch(e){
95
- console.log(JOE.Utils.color('[schema]event error: ','error')+event+':'+e);
96
- }
97
- }else{
98
- logit('no event'+event+' for '+schemaname );
99
- }
100
- }
101
- })
102
- }else{
103
- logit('schema "'+schemaname+'" not found');
104
- }
105
- }catch(e){
106
- console.log(JOE.Utils.color('[schema] hook error: ','error')+events+':'+e);
107
- }
108
- }
109
- this.update();
110
- return self;
111
- }
1
+ var fs = require('fs');
2
+ var serverPath = '../';
3
+ function Schemas(){
4
+ var self = this;
5
+ this.schema = {};
6
+ this.schemaList =[];
7
+ this.raw_schemas = {};
8
+ this.summary = {};
9
+ this.summaryGeneratedAt = null;
10
+ this.updateDelay = 1200;
11
+ //this.core = {};
12
+ var internalSchemasDir = __dirname+'/../schemas';
13
+ //console.log('internal schema dir = '+internalSchemasDir);
14
+ JOE.schemaDir = schemaDir = JOE.appDir+'/'+JOE.webconfig.schemaDir+'/';
15
+
16
+ this.load = function(s){
17
+
18
+ //console.log('[schema] loading '+s);
19
+ var schema,raw_schema;
20
+ var origin = 'instance';
21
+ if(s.indexOf('.js') == -1){s+= '.js';}
22
+ try{
23
+ var schemapath = JOE.schemaDir + s;
24
+ fs.accessSync(schemapath, fs.F_OK);
25
+
26
+ }catch(e){
27
+ var schemapath = serverPath+'schemas/' + s;
28
+ origin = 'core';
29
+ }
30
+ try{
31
+ try{
32
+ delete require.cache[require.resolve(schemapath)];
33
+ }catch(e){
34
+ console.log('[schema][error] '+s+' not deleted');
35
+ }
36
+ schema = require(schemapath);
37
+ try { schema.__origin = origin; } catch(_e) {}
38
+
39
+ }catch(e){
40
+ schema = {error:'schema '+s+' not found'};
41
+ console.log(e,schema);
42
+ }
43
+
44
+ /*raw_schema = self.raw_schemas[s.replace('.js','')] = $c.merge({},schema);
45
+ if(raw_schema.events){
46
+ raw_schema.events = $c.merge({},raw_schema.events);
47
+ }*/
48
+ raw_schema = self.raw_schemas[s.replace('.js','')] = schema.duplicate(true);
49
+ JOE.Utils.stringFunctions(schema);
50
+ let schemaname = s.replace('.js','')
51
+ self.schema[schemaname] = schema;
52
+ self.schemaList.push(schemaname);
53
+ return schema;
54
+ }
55
+ this.updateTimeout;
56
+ // Build normalized schema summaries for agents and tooling
57
+ this.buildSummary = function(){
58
+ try{
59
+ var summaries = {};
60
+ var MAX_META_LEN = 160;
61
+ function truncMeta(v){
62
+ if (typeof v !== 'string') return v;
63
+ return (v.length > MAX_META_LEN) ? (v.slice(0, MAX_META_LEN-1) + '…') : v;
64
+ }
65
+ // First pass: collect outbound relationships and field basics
66
+ Object.keys(self.schema||{}).forEach(function(name){
67
+ var def = self.schema[name] || {};
68
+ var fieldDefs = [];
69
+ var outbound = [];
70
+ var searchable = Array.isArray(def.searchables) ? def.searchables.slice() : [];
71
+ var allowedSorts = [];
72
+ var defaultSort = null;
73
+
74
+ // Sorters
75
+ if (Array.isArray(def.sorter)){
76
+ def.sorter.forEach(function(s){
77
+ var fieldName = null, dir = 'asc';
78
+ if (typeof s === 'string'){
79
+ fieldName = s.replace(/^!/, function(){ dir='desc'; return ''; });
80
+ } else if (s && typeof s === 'object' && typeof s.field === 'string'){
81
+ fieldName = s.field.replace(/^!/, function(){ dir='desc'; return ''; });
82
+ }
83
+ if (fieldName){
84
+ allowedSorts.push(fieldName);
85
+ if (!defaultSort) { defaultSort = { field: fieldName, dir: dir }; }
86
+ }
87
+ });
88
+ }
89
+ if (!defaultSort){ defaultSort = { field: 'joeUpdated', dir: 'desc' }; }
90
+
91
+ // Fields (robust extraction)
92
+ var rawFields = [];
93
+ try{
94
+ if (Array.isArray(def.fields)) { rawFields = def.fields; }
95
+ else if (typeof def.fields === 'function') { rawFields = def.fields.call(def) || []; }
96
+ }catch(_e){ rawFields = []; }
97
+
98
+ function inferRef(fieldObj){
99
+ var tSchema = null, isRef = false, isArr = false;
100
+ var t = fieldObj.type || 'unknown';
101
+ if (t === 'objectList' || t === 'array') { isArr = true; }
102
+ if (t === 'select'){
103
+ if (typeof fieldObj.schema === 'string'){ isRef = true; tSchema = fieldObj.schema; }
104
+ else if (typeof fieldObj.values === 'string'){ isRef = true; tSchema = fieldObj.values; }
105
+ else if (typeof fieldObj.goto === 'string'){ isRef = true; tSchema = fieldObj.goto; }
106
+ }
107
+ // Heuristics by name
108
+ var n = fieldObj.name || '';
109
+ if (!isRef && typeof n === 'string'){
110
+ if (n === 'status'){ isRef = true; tSchema = 'status'; }
111
+ else if (n === 'project'){ isRef = true; tSchema = 'project'; }
112
+ else if (n === 'assignee'){ isRef = true; tSchema = 'user'; }
113
+ else if (n === 'members'){ isRef = true; tSchema = 'user'; isArr = true; }
114
+ else if (n === 'tags'){ isRef = true; tSchema = 'tag'; isArr = true; }
115
+ else if (/_id$/.test(n)){ isRef = true; tSchema = n.replace(/_id$/, ''); }
116
+ else if (/Id$/.test(n)){ isRef = true; tSchema = n.replace(/Id$/, ''); }
117
+ }
118
+ return { isReference: !!isRef, targetSchema: tSchema, isArray: !!isArr };
119
+ }
120
+
121
+ function pushFieldEntry(entry){
122
+ if (!entry) return;
123
+ if (typeof entry === 'string'){
124
+ // string shorthand
125
+ var refGuess = inferRef({ name: entry, type: 'string' });
126
+ fieldDefs.push({ name: entry, type: 'string', isArray: false, isReference: refGuess.isReference, targetSchema: refGuess.targetSchema });
127
+ if (refGuess.isReference && refGuess.targetSchema){ outbound.push({ field: entry, targetSchema: refGuess.targetSchema }); }
128
+ return;
129
+ }
130
+ if (typeof entry === 'object'){
131
+ // skip layout markers
132
+ if (entry.section_start || entry.section_end || entry.sidebar_start || entry.sidebar_end) { return; }
133
+ // Treat extend as a real field mapped to core definition with specs overlay
134
+ if (entry.extend){
135
+ try{
136
+ var fname = entry.extend;
137
+ var coreDef = (JOE && JOE.Fields && (JOE.Fields.core || JOE.Fields.fields) && (JOE.Fields.core[fname] || JOE.Fields.fields[fname])) || null;
138
+ var inferredType = (entry.specs && entry.specs.type) || (coreDef && coreDef.type) || 'string';
139
+ var ref2 = inferRef({ name: fname, type: inferredType });
140
+ var fobjE = { name: fname, type: inferredType, isArray: !!(coreDef && coreDef.isArray) || (inferredType==='objectList'), isReference: ref2.isReference, targetSchema: ref2.targetSchema };
141
+ if (entry.specs && entry.specs.display) { fobjE.display = entry.specs.display; }
142
+ else if (coreDef && coreDef.display) { fobjE.display = coreDef.display; }
143
+ if (entry.specs && entry.specs.comment) { fobjE.comment = truncMeta(entry.specs.comment); }
144
+ else if (coreDef && coreDef.comment) { fobjE.comment = truncMeta(coreDef.comment); }
145
+ if (entry.specs && entry.specs.tooltip) { fobjE.tooltip = truncMeta(entry.specs.tooltip); }
146
+ else if (coreDef && coreDef.tooltip) { fobjE.tooltip = truncMeta(coreDef.tooltip); }
147
+ fieldDefs.push(fobjE);
148
+ if (ref2.isReference && ref2.targetSchema){ outbound.push({ field: fname, targetSchema: ref2.targetSchema, cardinality: (fobjE.isArray ? 'many' : 'one') }); }
149
+ return;
150
+ }catch(_e){ /* fall through */ }
151
+ }
152
+ if (entry.name){
153
+ var t = entry.type || 'unknown';
154
+ var ref = inferRef(entry);
155
+ var fobj = { name: entry.name, type: t, isArray: !!ref.isArray || t === 'objectList', isReference: ref.isReference, targetSchema: ref.targetSchema };
156
+ if (entry.display) { fobj.display = entry.display; }
157
+ if (entry.comment) { fobj.comment = truncMeta(entry.comment); }
158
+ if (entry.tooltip) { fobj.tooltip = truncMeta(entry.tooltip); }
159
+ fieldDefs.push(fobj);
160
+ if (ref.isReference && ref.targetSchema){ outbound.push({ field: entry.name, targetSchema: ref.targetSchema, cardinality: (ref.isArray || t === 'objectList') ? 'many' : 'one' }); }
161
+ }
162
+ // If objectList has properties, we don't descend (embedded), keep top-level only
163
+ }
164
+ }
165
+
166
+ rawFields.forEach(pushFieldEntry);
167
+ // Ensure core timestamps/ids appear even if not declared explicitly
168
+ ['_id','itemtype','joeUpdated','created'].forEach(function(coreName){
169
+ if (!fieldDefs.some(function(fd){ return fd.name === coreName; })){
170
+ fieldDefs.push({ name: coreName, type: coreName === '_id' ? 'string' : (coreName==='joeUpdated'||coreName==='created'?'date-time':'string') });
171
+ }
172
+ });
173
+
174
+ // Label field guess
175
+ var labelField = null;
176
+ ['name','title','label'].some(function(c){
177
+ var has = fieldDefs.some(function(fd){ return fd && fd.name === c; });
178
+ if (has){ labelField = c; return true; }
179
+ return false;
180
+ });
181
+
182
+ // Ensure common sorts appear
183
+ ['joeUpdated','created','name'].forEach(function(sf){ if (allowedSorts.indexOf(sf) === -1) { allowedSorts.push(sf); } });
184
+
185
+ var baseSummary = {
186
+ description: (def.summary && typeof def.summary.description === 'string') ? def.summary.description : '',
187
+ purpose: (def.summary && typeof def.summary.purpose === 'string') ? def.summary.purpose : '',
188
+ wip: !!def.wip,
189
+ source: (def.summary && typeof def.summary.source === 'string') ? def.summary.source : ((def.__origin === 'core') ? 'core' : 'instance'),
190
+ labelField: labelField || 'name',
191
+ defaultSort: defaultSort,
192
+ searchableFields: searchable,
193
+ allowedSorts: allowedSorts,
194
+ relationships: { outbound: outbound, inbound: { graphRef: 'server/relationships.graph.json' } },
195
+ fields: fieldDefs,
196
+ fieldCountBase: fieldDefs.length,
197
+ joeManagedFields: ['created','joeUpdated']
198
+ };
199
+ // Allow curated overrides from schema.summary
200
+ var finalSummary = baseSummary;
201
+ if (def.summary && typeof def.summary === 'object'){
202
+ // Shallow merge top-level props, but merge fields by name (do not replace entire list)
203
+ var curated = def.summary;
204
+ finalSummary = Object.assign({}, baseSummary, curated);
205
+ try{
206
+ var mergedFields = (baseSummary.fields||[]).slice();
207
+ var byName = {};
208
+ mergedFields.forEach(function(f, idx){ byName[f.name] = { idx: idx, field: f }; });
209
+ if (Array.isArray(curated.fields)){
210
+ curated.fields.forEach(function(cf){
211
+ if (!cf || !cf.name) return;
212
+ if (byName[cf.name]){
213
+ // overlay curated props onto base field
214
+ var existing = mergedFields[byName[cf.name].idx];
215
+ mergedFields[byName[cf.name].idx] = Object.assign({}, existing, cf);
216
+ } else {
217
+ mergedFields.push(cf);
218
+ }
219
+ });
220
+ }
221
+ finalSummary.fields = mergedFields;
222
+ }catch(_e){ finalSummary.fields = baseSummary.fields; }
223
+ }
224
+ // Enrich curated fields with display/comment/tooltip from base when missing
225
+ try{
226
+ var baseFieldByName = {};
227
+ (baseSummary.fields||[]).forEach(function(f){ baseFieldByName[f.name] = f; });
228
+ (finalSummary.fields||[]).forEach(function(f){
229
+ var bf = baseFieldByName[f.name];
230
+ if (!bf) return;
231
+ if (f.display == null && bf.display != null) f.display = bf.display;
232
+ if (f.comment == null && bf.comment != null) f.comment = truncMeta(bf.comment);
233
+ if (f.tooltip == null && bf.tooltip != null) f.tooltip = truncMeta(bf.tooltip);
234
+ if (f.comment) f.comment = truncMeta(f.comment);
235
+ if (f.tooltip) f.tooltip = truncMeta(f.tooltip);
236
+ });
237
+ }catch(_e){}
238
+
239
+ // Final counts
240
+ try{
241
+ finalSummary.fieldCount = Array.isArray(finalSummary.fields) ? finalSummary.fields.length : 0;
242
+ if (typeof finalSummary.fieldCountBase !== 'number'){
243
+ finalSummary.fieldCountBase = baseSummary.fieldCountBase || 0;
244
+ }
245
+ }catch(_e){}
246
+
247
+ summaries[name] = finalSummary;
248
+ });
249
+
250
+ // Inbound relationships will be maintained in a shared graph file; do not compute here
251
+
252
+ self.summary = summaries;
253
+ self.summaryGeneratedAt = new Date();
254
+ }catch(e){
255
+ console.log('[Schemas] buildSummary error:', e);
256
+ }
257
+ }
258
+
259
+ this.update = function(doItNow){
260
+ if(doItNow){
261
+ var lBM = new Benchmarker();
262
+ var schema_to_load = [];
263
+ //go through all the server files, then the schemadir files\
264
+ schema_to_load = schema_to_load.concat(fs.readdirSync(internalSchemasDir));
265
+ //schema_to_load = schema_to_load.concat(fs.readdirSync('server/schemas'));
266
+ schema_to_load = schema_to_load.concat(fs.readdirSync(JOE.schemaDir));
267
+ schema_to_load.map(function(sc){
268
+ self.load(sc);
269
+ })
270
+ // Build normalized summaries after load
271
+ try { self.buildSummary(); } catch(_e){}
272
+ logit(JOE.Utils.color('[schema] ','module')+schema_to_load.length+' updated in '+lBM.stop()+' secs');
273
+ }else{
274
+ clearTimeout(self.updateTimeout);
275
+ self.updateTimeout = setTimeout(self.update,self.updateDelay,true);
276
+ //logit(JOE.Utils.color('[schema] ','module')+'updating in '+ this.updateDelay/1000);
277
+
278
+ }
279
+ }
280
+ JOE.Utils.setupFileFolder(schemaDir,'schemas',self.update);
281
+ fs.watch(internalSchemasDir,function(){
282
+ self.update();
283
+ });
284
+
285
+ this.events = function(item,events,specs){
286
+ try{
287
+ var specs = specs || {};
288
+ var schemaprop = specs.schemaprop || "itemtype";
289
+ var schemaname = item[schemaprop];
290
+ var schema_def = self.raw_schemas[schemaname];
291
+
292
+ if(schema_def){
293
+
294
+ if(typeof events == "string"){
295
+ events = events.split(',');
296
+ }
297
+ events.map(function(event){
298
+ if(schema_def.events && schema_def.events[event]){
299
+ if(typeof schema_def.events[event] == "function"){
300
+ try{
301
+
302
+ schema_def.events[event](item,specs);
303
+ logit('[event] '+schemaname+' > '+event);
304
+ }catch(e){
305
+ console.log(JOE.Utils.color('[schema]event error: ','error')+event+':'+e);
306
+ }
307
+ }else{
308
+ logit('no event'+event+' for '+schemaname );
309
+ }
310
+ }
311
+ })
312
+ }else{
313
+ logit('schema "'+schemaname+'" not found');
314
+ }
315
+ }catch(e){
316
+ console.log(JOE.Utils.color('[schema] hook error: ','error')+events+':'+e);
317
+ }
318
+ }
319
+ this.update();
320
+ return self;
321
+ }
112
322
  module.exports = new Schemas();
@@ -156,12 +156,18 @@ server.get('/mcp-test.html',auth,function(req,res){
156
156
  server.get('/mcp-export.html',auth,function(req,res){
157
157
  res.sendFile(path.join(JOE.joedir,'_www','mcp-export.html'));
158
158
  });
159
+ server.get('/mcp-schemas.html',auth,function(req,res){
160
+ res.sendFile(path.join(JOE.joedir,'_www','mcp-schemas.html'));
161
+ });
159
162
  server.get(JOE.webconfig.joepath+'_www/mcp-test.html',auth,function(req,res){
160
163
  res.sendFile(path.join(JOE.joedir,'_www','mcp-test.html'));
161
164
  });
162
165
  server.get(JOE.webconfig.joepath+'_www/mcp-export.html',auth,function(req,res){
163
166
  res.sendFile(path.join(JOE.joedir,'_www','mcp-export.html'));
164
167
  });
168
+ server.get(JOE.webconfig.joepath+'_www/mcp-schemas.html',auth,function(req,res){
169
+ res.sendFile(path.join(JOE.joedir,'_www','mcp-schemas.html'));
170
+ });
165
171
 
166
172
  server.use(JOE.webconfig.joepath,express.static(JOE.joedir));
167
173
 
@@ -528,23 +534,28 @@ server.get(['/API/schema/:names','/API/schemas/:names'],auth,function(req,res,ne
528
534
  }else if(!JOE.Data || !JOE.Data.user || !JOE.Data.user.length){
529
535
  isNew = true;
530
536
  }
537
+ var useSummary = (req.query.summaryOnly === 'true' || req.query.summaryOnly === true || req.query.summaries === 'true' || req.query.view === 'summary');
531
538
  schemaname.map(function(s){
532
539
  if(isSuper || isNew || permitted.indexOf(s) != -1){
533
- payload[s] = JOE.Schemas.schema[s];
534
- }
535
- });
536
- var F = (req.query.fields||req.params.fields||'core').split(',');
537
- F.map(function(f){
538
- try {
539
- fieldspath = serverPath+'fields/' + f + '.js';
540
- delete require.cache[require.resolve(fieldspath)];
541
- fields = require(fieldspath);
542
- stringFunctions(fields);
543
- JOE.Fields[f]=fields;
544
- }catch(e){
545
- fields ={error:'field '+f+' not found'};
540
+ payload[s] = useSummary ? (JOE.Schemas.summary && JOE.Schemas.summary[s]) : JOE.Schemas.schema[s];
546
541
  }
547
542
  });
543
+ var Fraw = (req.query.fields||req.params.fields||'core');
544
+ var noFields = (Fraw === 'none') || (useSummary === true);
545
+ if (!noFields){
546
+ var F = Fraw.split(',');
547
+ F.map(function(f){
548
+ try {
549
+ fieldspath = serverPath+'fields/' + f + '.js';
550
+ delete require.cache[require.resolve(fieldspath)];
551
+ fields = require(fieldspath);
552
+ stringFunctions(fields);
553
+ JOE.Fields[f]=fields;
554
+ }catch(e){
555
+ fields ={error:'field '+f+' not found'};
556
+ }
557
+ });
558
+ }
548
559
 
549
560
  var bm = sBM.stop();
550
561
  switch(format){
@@ -555,7 +566,7 @@ server.get(['/API/schema/:names','/API/schemas/:names'],auth,function(req,res,ne
555
566
  res.send('var joe_info = '+js_payload);
556
567
  break;
557
568
  default:
558
- res.jsonp({schemas:payload,fields:fields,benchmark:bm});
569
+ res.jsonp({schemas:payload,fields:fields,benchmark:bm, summaryOnly: !!useSummary});
559
570
  break;
560
571
  }
561
572
 
@@ -673,7 +684,7 @@ server.get(['/API/save/','/API/save/:itemid'],auth,function(req,res,next){
673
684
  object[prop] = $c.parseBoolean(object[prop]);
674
685
  }
675
686
  }
676
- object.joeUpdated = new Date();
687
+ object.joeUpdated = new Date().toISOString();
677
688
  //TODO: check to make sure schema idprop is present
678
689
  JOE.Storage.save(object,object.itemtype,function(err,results){
679
690
  //var object = merge({},results);
@@ -97,6 +97,15 @@ function Storage(specs){
97
97
  var specs = $c.merge({history:true},(specs||{}));
98
98
  var user = specs.user || {name:'anonymous'};
99
99
 
100
+ // Ensure timestamps: always set joeUpdated; set created only on create when missing
101
+ try{
102
+ if(!data.joeUpdated){ data.joeUpdated = new Date().toISOString(); }
103
+ if(!data.created){
104
+ var cachedBefore = (data && data._id && JOE && JOE.Cache && JOE.Cache.findByID) ? JOE.Cache.findByID(collection,data._id) : null;
105
+ if(!cachedBefore){ data.created = new Date().toISOString(); }
106
+ }
107
+ }catch(_e){}
108
+
100
109
  if(JOE.Mongo){
101
110
  logit(moduleName+' mongo saving -> '+colorize(collection,'schema'));
102
111
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "generatedAt": null,
4
+ "schemas": {}
5
+ }
@@ -1,5 +1,42 @@
1
1
  var block = {
2
2
  info:"A block is a small chunk of reusable content, which can be customized when it is placed in a page or layout.",
3
+ // Curated summary for agents
4
+ summary:{
5
+ description:'Reusable content component or template used within layouts and pages.',
6
+ purpose:'Blocks encapsulate content or logic. They can be used directly or defined as templates (is_template) to generate other blocks.',
7
+ labelField:'name',
8
+ defaultSort:{ field:'name', dir:'asc' },
9
+ searchableFields:['name','info','_id'],
10
+ allowedSorts:['name','joeUpdated','created'],
11
+ relationships:{
12
+ outbound:[
13
+ { field:'site', targetSchema:'site', cardinality:'one' },
14
+ { field:'includes', targetSchema:'include', cardinality:'many' },
15
+ { field:'use_template', targetSchema:'block', cardinality:'one' }
16
+ ],
17
+ inbound:{ graphRef:'server/relationships.graph.json' }
18
+ },
19
+ joeManagedFields:['created','joeUpdated'],
20
+ fields:[
21
+ { name:'_id', type:'string', required:true },
22
+ { name:'itemtype', type:'string', required:true, const:'block' },
23
+ { name:'name', type:'string', required:true },
24
+ { name:'info', type:'string' },
25
+ { name:'description', type:'string' },
26
+ { name:'use_template', type:'string', isReference:true, targetSchema:'block' },
27
+ { name:'is_template', type:'boolean' },
28
+ { name:'template_type', type:'string', enumValues:['code','module'] },
29
+ { name:'template', type:'string' },
30
+ { name:'block_fields', type:'objectList' },
31
+ { name:'content_type', type:'string', enumValues:['wysiwyg','code','module'] },
32
+ { name:'content', type:'string' },
33
+ { name:'process_functions', type:'objectList' },
34
+ { name:'site', type:'string', isReference:true, targetSchema:'site' },
35
+ { name:'includes', type:'string', isArray:true, isReference:true, targetSchema:'include' },
36
+ { name:'joeUpdated', type:'string', format:'date-time', required:true },
37
+ { name:'created', type:'string', format:'date-time', required:true }
38
+ ]
39
+ },
3
40
  sorter:['name',{value:'!is_template',display:'template'}],
4
41
  subsets:function(){
5
42
  var subs = [];
@@ -17,7 +17,8 @@ var board = function(){return{
17
17
  'dataset',
18
18
  '_id','created','itemtype'
19
19
  ],
20
- idprop : "_id"
20
+ idprop : "_id",
21
+ wip:true
21
22
  }};
22
23
 
23
24
  module.exports = board();
@@ -2,6 +2,33 @@ var schema = {
2
2
  title : '${name}',
3
3
  info:"Use a budget to track your income and expenses.",
4
4
  menuicon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 -15 80 80"><path d="M18.5 2.6C14.4 2.6 11 5.9 11 10.1 11 11.2 11.3 12.3 11.8 13.3L13.5 12.4C13.2 11.7 13 10.9 13 10.1 13 7 15.4 4.6 18.5 4.6 21.5 4.6 24 7 24 10.1L26 10.1C26 5.9 22.6 2.6 18.5 2.6zM37.2 6.9A1 1 0 0 0 37.1 7 1 1 0 0 0 37 7C34.5 7.6 31.6 9.2 29.7 12.6 28 12.2 26.2 11.9 24.3 11.9 18.6 11.9 14.2 13.7 10.7 16.4 8.9 17.8 7.5 19.7 6.7 22 6.3 21.9 5.9 21.7 5.5 21.4 4.7 20.9 4 20.1 4 18.8 4 17.7 4.5 16.8 5.3 16.4A1 1 0 0 0 4.9 14.5 1 1 0 0 0 4.6 14.6C2.9 15.3 2 17 2 18.8 2 20.9 3.2 22.3 4.3 23.1 5 23.5 5.6 23.8 6.1 23.9 5.7 25.4 5.6 27 5.7 28.8 5.8 33.2 8.9 36.5 11.1 38.3A1 1 0 0 0 11.2 38.3C11.4 38.5 11.4 38.6 11.4 38.6 11.5 38.6 11.5 38.6 11.4 38.8 11.1 39.8 10.5 41.2 10.5 41.2A1 1 0 0 0 10.5 41.2C10 42.6 10.7 44.2 12.1 44.7A1 1 0 0 0 12.1 44.7L15.3 45.9C16.7 46.4 18.3 45.7 18.8 44.3 18.8 44.3 19.2 43.2 19.6 42.3 19.6 42.1 19.7 42.1 19.7 42.1 19.8 42.1 19.8 42.1 19.8 42.1 20.5 42.2 21.9 42.6 24.3 42.6 26.7 42.6 28 42.4 28.7 42.3 28.8 42.3 28.9 42.3 28.9 42.3 28.9 42.4 28.9 42.4 28.9 42.4 28.9 42.5 28.9 42.5 28.9 42.5 29.3 43.4 29.6 44.3 29.7 44.3 29.7 44.4 29.7 44.4 29.7 44.4 30.2 45.7 31.8 46.4 33.2 45.9L36.4 44.7A1 1 0 0 0 36.4 44.7C37.8 44.2 38.5 42.6 37.9 41.2L37.4 39.8A1 1 0 0 0 37.3 39.7C37.3 39.7 37.4 39.7 37.4 39.7 37.4 39.7 37.2 39.7 37.6 39.6 40 38.2 41.9 36.2 43 33.8 43.1 33.5 43.2 33.4 43.3 33.3 43.4 33.2 43.4 33.2 43.8 33.2L45.4 33.2C46.8 33.2 48 32 48 30.5L48 25.7C48 24.2 46.8 23 45.4 23L43.8 23C43.5 23 43.4 23 43.4 23 42.9 20.1 41 16.5 36.7 14.7 36.7 12.6 37.2 11 38.2 8.3A1 1 0 0 0 37.2 6.9zM35.6 9.8C35 11.6 34.6 13.3 34.6 15.3A1 1 0 0 0 35.3 16.2C39.5 17.7 41 20.9 41.4 23.4 41.6 24.4 42.6 25 43.8 25L45.4 25C45.7 25 46 25.3 46 25.7L46 30.5C46 30.9 45.7 31.2 45.4 31.2L43.8 31.2C43 31.2 42.4 31.4 41.9 31.8 41.5 32.2 41.3 32.6 41.2 32.9 40.2 35 38.7 36.7 36.6 37.8 35.8 38.2 35.4 39 35.4 39.6 35.4 40.1 35.5 40.4 35.6 40.5L35.6 40.5 35.6 40.7 36.1 41.9C36.2 42.3 36 42.7 35.7 42.8L32.4 44.1C32.1 44.2 31.6 44 31.5 43.6A1 1 0 0 0 31.5 43.6C31.5 43.6 31.1 42.7 30.8 41.8 30.7 41.4 30.5 40.9 30 40.6 29.5 40.3 29 40.3 28.6 40.3 27.7 40.4 26.5 40.6 24.3 40.6 22.1 40.6 21 40.3 20.2 40.1 19.8 40 19.2 40 18.7 40.3 18.1 40.6 17.9 41.1 17.8 41.6 17.4 42.5 17 43.6 17 43.6 16.8 44 16.4 44.2 16 44.1L12.8 42.8C12.4 42.7 12.3 42.3 12.4 41.9 12.4 41.9 13 40.5 13.3 39.5 13.5 38.8 13.4 38 13.2 37.6 12.9 37.1 12.5 36.8 12.4 36.8 10.4 35.1 7.8 32.3 7.7 28.7 7.4 23.6 9.2 20.1 11.9 18 15.1 15.5 19 13.9 24.3 13.9 26.3 13.9 28.2 14.2 30 14.6A1 1 0 0 0 31.1 14.1C32.2 11.6 33.9 10.5 35.6 9.8zM36.5 23.1C35.4 23.1 34.6 23.9 34.6 25 34.6 26.1 35.4 26.9 36.5 26.9 37.5 26.9 38.4 26.1 38.4 25 38.4 23.9 37.5 23.1 36.5 23.1z"/></svg>',
5
+ // Curated summary for agents
6
+ summary:{
7
+ description:'Spending or income target tracked against ledgers via tags.',
8
+ purpose:'Use budgets to set target amounts and analyze spending history. Budgets link to ledgers sharing the same tags.',
9
+ labelField:'name',
10
+ defaultSort:{ field:'joeUpdated', dir:'desc' },
11
+ searchableFields:['name','info','_id'],
12
+ allowedSorts:['joeUpdated','created','name','amount'],
13
+ relationships:{
14
+ outbound:[
15
+ { field:'tags', targetSchema:'tag', cardinality:'many' }
16
+ ],
17
+ inbound:{ graphRef:'server/relationships.graph.json' }
18
+ },
19
+ joeManagedFields:['created','joeUpdated'],
20
+ fields:[
21
+ { name:'_id', type:'string', required:true },
22
+ { name:'itemtype', type:'string', required:true, const:'budget' },
23
+ { name:'name', type:'string', required:true },
24
+ { name:'info', type:'string' },
25
+ { name:'additive', type:'boolean', display:'Sum actuals', comment:'Dynamic budget: amount equals the sum of matching transactions for the period; no remaining shown.' },
26
+ { name:'amount', type:'number' },
27
+ { name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
28
+ { name:'joeUpdated', type:'string', format:'date-time', required:true },
29
+ { name:'created', type:'string', format:'date-time', required:true }
30
+ ]
31
+ },
5
32
  listView:{
6
33
  title:
7
34
  '<joe-title class="joe-fright">-$${amount}</joe-title>'+
@@ -13,7 +40,7 @@ menuicon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 -15 80 80"><path
13
40
  fields:[
14
41
  'name',
15
42
  'info',
16
- 'additive:boolean',
43
+ {name:'additive', type:'boolean', display:'Sum actuals', comment:'If checked, this budget is dynamic: its amount equals the sum of matching transactions for the selected period. No remaining is shown. Use for roll‑up categories (e.g., income or passthrough).'},
17
44
  {name:'amount',type:'number',comment:'the default amount'},
18
45
  'tags',
19
46
  {sidebar_start:'right'},