json-object-editor 0.10.520 → 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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.10.521
2
+ - MCP
3
+ - Added `saveObjects` batch save tool (bounded concurrency, stopOnError). Updated MCP Test preset and Export starter instructions.
4
+
1
5
  ## 0.10.520
2
6
  - added mcp schemas page
3
7
  - summarized core schemas
@@ -20,6 +24,8 @@
20
24
  - MCP Prompt now loads system instructions from `docs/joe_agent_custom_gpt_instructions_v_2.md`
21
25
  - Added docs: `joe_agent_spec_v_2.2.md`, `schema_summary_guidelines.md`
22
26
 
27
+
28
+
23
29
  - Summaries added/refined
24
30
  - Sitebuilder: `site`, `page`, `layout`, `block`, `include`, `post`
25
31
  - Finance: `transaction`, `ledger`, `financial_account`, `budget` (additive clarified)
@@ -197,19 +197,20 @@
197
197
  '- Ask one brief clarification only if a required parameter is missing.',
198
198
  '- On a new session: call hydrate {} first, then proceed.',
199
199
  '- Keep results scoped (limit 10–25). Flatten is optional and off by default; enable only when needed.',
200
- '- Never expose secrets/tokens. Confirm with the user before saveObject.',
200
+ '- Never expose secrets/tokens. Confirm with the user before saveObject/saveObjects.',
201
201
  '',
202
202
  'Typical flow:',
203
203
  '- listSchemas {}, getSchema { "name": "<schema>" }',
204
204
  '- search { "query": { "itemtype": "<schema>" }, "limit": 10 } (cache) or { "source": "storage" } when authoritative results are needed',
205
205
  '- getObject { "_id": "<id>", "schema": "<schema>" } for a single item',
206
- '- saveObject { "object": { ... } } only on explicit user request',
206
+ '- saveObject { "object": { ... } } or saveObjects { "objects": [ ... ], "concurrency": 5 } only on explicit user request',
207
207
  '',
208
208
  'Examples:',
209
209
  '- listSchemas {}',
210
210
  '- getSchema { "name": "client" }',
211
211
  '- search { "schema": "client", "source": "storage", "query": { "status": "active" }, "limit": 10 }',
212
- '- getObject { "_id": "123", "schema": "client" }'
212
+ '- getObject { "_id": "123", "schema": "client" }',
213
+ '- saveObjects { "objects": [{ "itemtype":"client", "name":"Batch A" }], "stopOnError": false, "concurrency": 5 }'
213
214
  ].join('\n');
214
215
  $('starter').value = starter;
215
216
 
package/_www/mcp-nav.js CHANGED
@@ -1,17 +1,20 @@
1
1
  ;(function(){
2
2
  function buildNav(){
3
3
  var nav = document.createElement('nav');
4
- nav.setAttribute('style','display:flex;gap:10px;align-items:center;margin-bottom:8px');
5
- nav.innerHTML = [
4
+ nav.setAttribute('style','display:flex;flex-direction:column;gap:6px;margin-bottom:8px');
5
+ var infoRow = '<div id="mcp-nav-info" class="small" style="opacity:.8"></div>';
6
+ var linksRow = [
7
+ '<div style="display:flex;gap:10px;align-items:center">',
6
8
  '<a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a>',
7
9
  '<a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a>',
8
10
  '<a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a>',
9
11
  '<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
10
12
  '<span style="margin-left:auto"></span>',
11
- '<span id="mcp-nav-info" class="small" style="opacity:.8;margin-right:10px"></span>',
12
13
  '<a href="/privacy" target="privacy_win" rel="noopener">Privacy</a>',
13
- '<a href="/terms" target="terms_win" rel="noopener">Terms</a>'
14
+ '<a href="/terms" target="terms_win" rel="noopener">Terms</a>',
15
+ '</div>'
14
16
  ].join('');
17
+ nav.innerHTML = infoRow + linksRow;
15
18
  return nav;
16
19
  }
17
20
  function insert(){
@@ -44,6 +44,7 @@
44
44
  <button id="presetSearchSlimRecent">search clients (slim, recent)</button>
45
45
  <button id="presetSearchWithCount">search clients (withCount)</button>
46
46
  <button id="presetSearchCountOnly">search clients (countOnly)</button>
47
+ <button id="presetSaveObjects">saveObjects: batch save (example)</button>
47
48
  </div>
48
49
 
49
50
  <h3>Call JSON-RPC</h3>
@@ -75,6 +76,7 @@
75
76
  const presetSearchSlimRecent = $('presetSearchSlimRecent');
76
77
  const presetSearchWithCount = $('presetSearchWithCount');
77
78
  const presetSearchCountOnly = $('presetSearchCountOnly');
79
+ const presetSaveObjects = $('presetSaveObjects');
78
80
 
79
81
  // Try to infer base from window location
80
82
  base.value = base.value || (location.origin);
@@ -203,6 +205,20 @@
203
205
  renderToolInfo();
204
206
  params.value = JSON.stringify({ schema: 'client', source: 'cache', query: { itemtype: 'client' }, countOnly: true }, null, 2);
205
207
  };
208
+
209
+ presetSaveObjects.onclick = function(){
210
+ toolSel.value = 'saveObjects';
211
+ renderToolInfo();
212
+ // Example: two minimal objects; adjust itemtype/fields as needed
213
+ params.value = JSON.stringify({
214
+ objects: [
215
+ { itemtype: "client", name: "Batch Client A" },
216
+ { itemtype: "client", name: "Batch Client B" }
217
+ ],
218
+ stopOnError: false,
219
+ concurrency: 5
220
+ }, null, 2);
221
+ };
206
222
 
207
223
  callBtn.onclick = async function(){
208
224
  setStatus(callStatus, 'Calling...', null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.520",
3
+ "version": "0.10.521",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
package/readme.md CHANGED
@@ -1,470 +1,526 @@
1
- <img src="http://joe.craydent.com/JsonObjectEditor/img/svgs/joe_banner_o.svg"/>
2
-
3
- # Json Object Editor
4
- JOE is software that allows you to manage data models via JSON objects. There are two flavors, the client-side version and nodejs server platform.
5
-
6
-
7
-
8
- ## Architecture & Mental Model (Server)
9
-
10
- - Global JOE
11
- - Created in `server/init.js`; exposes subsystems like `JOE.Utils`, `JOE.Schemas`, `JOE.Storage`, `JOE.Mongo`/`JOE.MySQL`, `JOE.Cache`, `JOE.Apps`, `JOE.Server` (Express), `JOE.Sites`, `JOE.io` (Socket), and `JOE.auth`.
12
- - Init pattern
13
- - Modules can optionally export `init()`; `init.js` loads/watches modules and calls `init()` after `JOE.Server` is ready. This enables hot-reload and keeps `Server.js` thin.
14
- - Schemas & events
15
- - `JOE.Schemas` loads from `server/schemas/` and app schema dir into `JOE.Schemas.schema` with names in `JOE.Schemas.schemaList`. Raw copies live in `JOE.Schemas.raw_schemas` for event hooks.
16
- - `JOE.Storage.save()` triggers schema events (`create`, `save`, `status`, `delete`) via `JOE.Schemas.events()` and writes history documents to `_history`.
17
- - Storage & cache
18
- - `JOE.Storage.load(collection, query, cb)` chooses backend per schema `storage.type` (`mongo`, `mysql`, `file`, `api`), with Mongo/file fallback.
19
- - `JOE.Storage.save(item, collection, cb, { user, history })` emits `item_updated` to sockets, records history, and fires events.
20
- - `JOE.Cache.update(cb, collections)` populates `JOE.Data`, flattens a `list`, and builds `lookup`. Use `JOE.Cache.findByID(collection, idOrCsv)` for fast lookups; `JOE.Cache.search(query)` for in-memory filtering.
21
- - Auth
22
- - `JOE.auth` middleware checks cookie token or Basic Auth; many API routes (and `/mcp`) are protected.
23
- - Shorthand `$J`
24
- - Server and client convenience: `$J.get(_id)`, `$J.search(query)`, `$J.schema(name)`.
25
- - On server, provided by `server/modules/UniversalShorthand.js` and assigned to `global.$J` in `init.js`.
26
- - MCP overview
27
- - Manifest: `/.well-known/mcp/manifest.json`; JSON-RPC: `POST /mcp` (auth-protected). Tools map to real JOE APIs (`Schemas`, `Storage`, `Cache`) and sanitize sensitive fields.
28
-
29
- ## MCP routes and local testing
30
-
31
- - Endpoints
32
- - Manifest (public): `GET /.well-known/mcp/manifest.json`
33
- - JSON-RPC (auth): `POST /mcp`
34
- - Privacy (public): `/privacy` (uses Setting `PRIVACY_CONTACT` for contact email)
35
- - Terms (public): `/terms`
36
-
37
- - Auth
38
- - If users exist, `POST /mcp` requires cookie or Basic Auth (same as other APIs). If no users configured, it is effectively open.
39
-
40
- - Test page
41
- - JOE ships a simple tester at `/_www/mcp-test.html` inside the package.
42
- - Access via JOE path: `http://localhost:<PORT>/JsonObjectEditor/_www/mcp-test.html`
43
- - If your host app serves its own `_www`, the tester can also be available at the root (fallback) if running with the updated server that mounts JOE’s `_www` as a secondary static directory. Then: `http://localhost:<PORT>/mcp-test.html`
44
-
45
- - Tools
46
- - `listSchemas(name?)`, `getSchema(name)`
47
- - `getObject(_id, schema?)` (supports optional `flatten` and `depth`)
48
- - `search` (exact): unified tool for cache and storage
49
- - Params: `{ schema?, query?, ids?, source?: 'cache'|'storage', limit?, flatten?, depth? }`
50
- - Defaults to cache across all collections; add `schema` to filter; set `source:"storage"` to query a specific schema in the DB. Use `fuzzySearch` for typo-tolerant free text.
51
- - `fuzzySearch` (typo-tolerant free text across weighted fields)
52
- - Params: `{ schema?, q, filters?, fields?, threshold?, limit?, offset?, highlight?, minQueryLength? }`
53
- - Defaults: `fields` resolved from schema `searchables` (plural) if present; otherwise weights `name:0.6, info:0.3, description:0.1`. `threshold:0.5`, `limit:50`, `minQueryLength:2`.
54
- - Returns: `{ items, count }`. Each item may include `_score` (0..1) and `_matches` when `highlight` is true.
55
- - `saveObject({ object })`
56
-
57
- - Quick tests (PowerShell)
58
- - Prepare headers if using Basic Auth:
59
- ```powershell
60
- $pair = "user:pass"; $b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
61
- $h = @{ Authorization = "Basic $b64"; "Content-Type" = "application/json" }
62
- $base = "http://localhost:<PORT>"
63
- ```
64
- - Manifest:
65
- ```powershell
66
- Invoke-RestMethod "$base/.well-known/mcp/manifest.json"
67
- ```
68
- - listSchemas:
69
- ```powershell
70
- $body = @{ jsonrpc="2.0"; id="1"; method="listSchemas"; params=@{} } | ConvertTo-Json -Depth 6
71
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
72
- ```
73
- - getSchema:
74
- ```powershell
75
- $body = @{ jsonrpc="2.0"; id="2"; method="getSchema"; params=@{ name="<schemaName>" } } | ConvertTo-Json -Depth 6
76
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
77
- ```
78
- - search (cache default):
79
- ```powershell
80
- $body = @{ jsonrpc="2.0"; id="3"; method="search"; params=@{ query=@{ itemtype="<schemaName>" }; limit=10 } } | ConvertTo-Json -Depth 10
81
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
82
- ```
83
- - fuzzySearch (cache):
84
- ```powershell
85
- $body = @{ jsonrpc="2.0"; id="6"; method="fuzzySearch"; params=@{ schema="<schemaName>"; q="st paal"; threshold=0.35; limit=10 } } | ConvertTo-Json -Depth 10
86
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
87
- ```
88
- - search (storage):
89
- ```powershell
90
- $body = @{ jsonrpc="2.0"; id="4"; method="search"; params=@{ schema="<schemaName>"; source="storage"; query=@{ }; limit=10 } } | ConvertTo-Json -Depth 10
91
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
92
- ```
93
- - search (ids + flatten):
94
- ```powershell
95
- $body = @{ jsonrpc="2.0"; id="5"; method="search"; params=@{ schema="<schemaName>"; ids=@("<id1>","<id2>"); flatten=$true; depth=2 } } | ConvertTo-Json -Depth 10
96
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
97
- ```
98
- - saveObject:
99
- ```powershell
100
- $object = @{ itemtype="<schemaName>"; name="Test via MCP" }
101
- $body = @{ jsonrpc="2.0"; id="4"; method="saveObject"; params=@{ object=$object } } | ConvertTo-Json -Depth 10
102
- Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
103
- ```
104
-
105
- - Quick tests (curl)
106
- - Manifest:
107
- ```bash
108
- curl -s http://localhost:<PORT>/.well-known/mcp/manifest.json | jq
109
- ```
110
- - listSchemas:
111
- - search (cache):
112
- ```bash
113
- curl -s -X POST http://localhost:<PORT>/mcp \
114
- -H 'Content-Type: application/json' \
115
- -d '{"jsonrpc":"2.0","id":"3","method":"search","params":{"query":{"itemtype":"<schemaName>"},"limit":10}}' | jq
116
- ```
117
- - fuzzySearch:
118
- ```bash
119
- curl -s -X POST http://localhost:<PORT>/mcp \
120
- -H 'Content-Type: application/json' \
121
- -d '{"jsonrpc":"2.0","id":"6","method":"fuzzySearch","params":{"schema":"<schemaName>","q":"st paal","threshold":0.35,"limit":10}}' | jq
122
- ```
123
- - search (storage):
124
- ```bash
125
- curl -s -X POST http://localhost:<PORT>/mcp \
126
- -H 'Content-Type: application/json' \
127
- -d '{"jsonrpc":"2.0","id":"4","method":"search","params":{"schema":"<schemaName>","source":"storage","query":{},"limit":10}}' | jq
128
- ```
129
- ```bash
130
- curl -s -X POST http://localhost:<PORT>/mcp \
131
- -H 'Content-Type: application/json' \
132
- -d '{"jsonrpc":"2.0","id":"1","method":"listSchemas","params":{}}' | jq
133
- ```
134
-
135
- - Troubleshooting
136
- - If you see a payload like `{ originalURL: "/...", site: "no site found" }`, the request hit the Sites catch-all. Ensure MCP routes are initialized before Sites (handled by default in `server/init.js` via `MCP.init()`), and use the correct URL: `/.well-known/mcp/manifest.json` or `/mcp`.
137
- - To update contact email on /privacy and /terms, set Setting `PRIVACY_CONTACT`.
138
-
139
- ## SERVER/PLATFORM mode
140
- check port 2099
141
- /JOE/
142
- /JsonObjectEditor/docs.html
143
- *Should auto-open on start
144
-
145
- Json Object Editor (Universal-esque software)
146
- (requires connection to a mongo server for full functionality)
147
-
148
-
149
-
150
- ### JOE server instantiation (add to entry point js file)
151
- var joe = require('json-object-editor');
152
- or here's a custom example
153
- var joe = require('json-object-editor')({
154
- name:'name_your_joe' (JOE),
155
- joedb:'local_custom_database_name' // {{mongoURL}}' if using mongolab or remote mongodb,
156
- port:'4099', (2099)
157
- socketPort:'4098', (2098)
158
- sitesPort:'4100' (2100),
159
- clusters:1,
160
- hostname:'server name if not localhost'(localhost)
161
- });
162
-
163
- ## $J (universal) Shorthand JOE
164
- $J to access helper funtions on client and server. (callbacks are optional)
165
- $J.get(itemId,[callback])
166
- $J.schema(schemaname,[callback])
167
-
168
- # Client-Side (front end only)
169
-
170
- ## js client only instantiation
171
- var specs = {
172
- fields:{
173
- species:{label:'Species',type:'select', values:['cat','dog','rat','thing'], onchange:adjustSchema},
174
- gender:{type:'select', values:['male','female']},
175
- legs:{label:'# of Legs',type:'int', onblur:logit},
176
- weight:{label:' Weight (lbs)',type:'number', onblur:logit},
177
- name:{label:' pet Name', onkeyup:logValue},
178
- //id:{label:'ID',type:'text', locked:true},
179
- id:{label:'ID',type:'guid'},
180
-
181
- //example of select that takes function (function is passed item)
182
- animalLink:{label:'Link to other animal',type:'select', values:getAnimals},
183
- hiddenizer:{hidden:true}
184
- },
185
- schemas:{
186
- animal:animalschema,
187
- thing:thingschema
188
- },
189
- container:string ('body'),
190
- compact:false,
191
- useBackButton:true,
192
- autosave:2000,
193
- listSubMenu:{filters:{}},
194
- useHashlink:true,
195
- title:'${itemtype} | ${display}'
196
- }
197
- var JOE = new JsonObjectEditor(specs);
198
- JOE.init();
199
-
200
- ##JOE CONFIG
201
- ##specs
202
- - useBackButton:[false] if true, back button moves through joe panels when joe has history to go to (is open).
203
- - useHashlink:[false], true or a template for hashlinks.
204
- default template is '${schema_name}_${_id}'
205
- default server tempalte is '${schema_name}/${_id}'
206
- ##SCHEMA CONFIG
207
- ###fields
208
- Properties for all Fields
209
-
210
- - `label / display`: what the field should display as
211
- *If the field type is boolean, label controls checkbox/boolean label
212
- - `value`: default value if not one in object
213
- - `default`: default value for field || function(object)
214
- - `type`: what type of field should JOE show
215
- - `hidden`: boolean / function, value will be added (but unsees by user)
216
- - `locked`: boolean
217
- - `condition`: boolean
218
- - `width`: used for layout control.
219
- - can use pixels or percentages (as string)
220
- - `comment`: a commentthat shows up at the beginning of the field
221
- - `tooltip`: hover/clickable tooltip that shows up next to name
222
-
223
- **field types:**
224
-
225
- - `rendering`: for css html and js
226
- - `text`: default single line text.
227
- - autocomplete: boolean // obj of specs (template, idprop)
228
- - values:array of possibilities
229
- -maxlength:string
230
- - `int`: integer field
231
- - `number`: number (float) field
232
- - `select`: select list.
233
- - multiple(bool)
234
- - values(array of objects, [{value:"",name/display:""]), can be a function
235
- - disabled:boolean(func acceptable)
236
- - idprop: string of prop name
237
- - `geo`: shows a map
238
- - *takes a string array "[lat,lon]"*
239
- - center:[lat,lon], center of map
240
- - zoom: zoom level (higher zooms in more)
241
- - returns "[lat,lon]"
242
- -`image` : shows an image and HxW as th image url is typed in.
243
- - `multisorter` : allows arrays of objects to be selected and sorted in right bin.
244
- - values(array of objects, [{value:"",name/display:""]), can be a function
245
- - `content` : show content on in the editor
246
- - run: function to be run(current_object,field_properties)
247
- - template: html template for fillTemplate(template,current_object);
248
- - `objectlist` : a table of objects with editable properties
249
- - properties: array of objects|strings for the object property names
250
- -name: value in object
251
- -display: header in objectList
252
- - max: integer, number or items that can be added. use zero for infinite.
253
- - hideHeadings: don't show table headings
254
- - `objectReference` : a list of object ids
255
- - template
256
- - autocomplete_template
257
- - idprop
258
- - values
259
- - max(0 unlimited)
260
- - sortable(true)
261
- - `code` :
262
- - language
263
-
264
- - `boolean`:
265
- - label:controls checkbox label
266
- - `preview` :
267
- -content: string or function(current joe object) to replace everything on page (template).
268
- -bodycontent: same as content, only replaces body content.
269
- -url: preview page if not the default one.
270
- - encoded: boolean, if pre uriencoded
271
- **labels:**
272
-
273
- - pass an object instead of a string to the fields array.
274
-
275
-
276
- {label:'Name of the following properties section'}
277
-
278
- ##page sections
279
- {section_start: 'SectionName',
280
- section_label:'Section Name with Labels',
281
- condition:function(item){
282
- return item.show;}
283
- },
284
- {section_end: 'CreativeBrief'}
285
-
286
- - pass an object instead of a string to the fields array. these show up on the details view as anchors.
287
-
288
-
289
- - Object Properties
290
- - `section_start`: name/id of section
291
- - `'section_label`:use instead of section_start for display name
292
- - `section_end`: name/id of section(str)
293
- - template: html template for fillTemplate(template,current_object);
294
-
295
- ##page sidebar
296
- {sidebar_start: 'SectionName',
297
- sidebar_label:'Section Name with Labels',
298
- condition:function(item){
299
- return item.show;}
300
- },
301
- {sidebar_end: 'CreativeBrief'}
302
-
303
- - pass an object instead of a string to the fields array. these show up on the details view as anchors.
304
-
305
-
306
- - Object Properties
307
- - `sidebar_start`: name/id of sidebar
308
- - `sidebar_label`:use instead of sidebar_start for display name
309
- - `sidebar_end`: name/id of sidebar(str)
310
- - template: html template for fillTemplate(template,current_object);
311
-
312
- ###defaultProfile
313
- overwrites the default profile
314
-
315
- #schemas
316
-
317
- a list of schema objects that can configure the editor fields, these can be given properties that are delegated to all the corresponding fields.
318
-
319
- var animalschema =
320
- {
321
- title:'Animal', *what shows as the panel header*
322
- fields:['id','name','legs','species','weight','color','gender','animalLink'], *list of visible fields*
323
- _listID:'id', *the id for finding the object*
324
- _listTitle:'${name} ${species}', *how to display items in the list*
325
- menu:[array of menu buttons],
326
- listMenuTitle: (string) template forjoe window title in list view,
327
- listmenu:[array of menu buttons] (multi-edit and select all always show),
328
- /*callback:function(obj){
329
- alert(obj.name);
330
- },*/
331
- onblur:logit,
332
-
333
- hideNumbers:boolean *toggle list numbers*
334
- multipleCallback:function to be called after a multi-edit. passed list of edited items.
335
- onUpdate: callback for after update. passed single edited items.
336
- onMultipleUpdate:callback for after multi update.passed list of edited items.
337
- filters: array of objects
338
- checkChanges:(bool) whether or not to check for changes via interval and on leave
339
- }
340
- ##Table View
341
- - add tableView object to a schema;
342
- -cols = [strings||objects]
343
- -string is the name and value
344
- -display/header is the column title
345
- -property/name = object property
346
- ###Pre-formating
347
- you can preformat at the joe call or schema level. The data item will be affected by the passed function (which should return the preformated item).
348
-
349
- ##menu##
350
- an array of menu buttons
351
-
352
- //the default save button
353
- //this is the dom object,
354
- //use _joe.current.object for current object
355
- condition:function(field,object) to call
356
- self = Joe object
357
- var __saveBtn__ = {name:'save',label:'Save', action:'_joe.updateObject(this);', css:'joe-save-button'};
358
-
359
- ##itemMenu##
360
- as array of buttons for each item in list views
361
- - name
362
- - action (action string)
363
- - url (instead of js action)
364
- - condition
365
-
366
- ##itemExpander##
367
- template or run for content to be shown under the main list item block.
368
-
369
-
370
- ###Addition properties
371
- **Changing the schema on the fly?**
372
-
373
- _joe.resetSchema(new schema name);
374
-
375
-
376
-
377
- **css (included) options**
378
-
379
- - joe-left-button
380
- - joe-right-button
381
-
382
- ##FIELDS
383
-
384
- {extend:'name',specs:{display:'Request Title'}},//extends the field 'name' with the specs provided.
385
- ##usage
386
- ### a | adding a new object
387
-
388
- _joe.show({},{schema:'animal',callback:addAnimal);
389
- //or goJoe(object,specs)
390
-
391
- ...
392
- function addAnimal(obj){
393
- animals.push(obj);
394
- }
395
-
396
- ### b | viewing a list of objects
397
-
398
- goJoe([array of objects],specs:{schema,subsets,subset})
399
- goJoe.show(animals,{schema:'animal',subsets:[{name:'Two-Legged',filter:{legs:2}}]});
400
- //use the specs property subset to pre-select a subset by name
401
-
402
- **properties**
403
-
404
- - _listWindowTitle: the title of the window (can be passed in with the schema);
405
- - _listCount: added to the current object and can be used in the title.
406
- - _listTitle:'${name} ${species}', *how to display items in the list*
407
- - _icon: [str] template for a list item icon (standard min 50x50), 'http://www.icons.com/${itemname}', can be obj with width, height, url
408
- - listSubMenu:a function or object that represents the list submenu
409
- - stripeColor:string or function that returns valid css color descriptor.
410
- - bgColor:string or function that returns valid css color descriptor.
411
- - subsets: name:string, filter:object
412
- - subMenu:a function or object that represents the single item submenu
413
-
414
- - _listTemplate: html template that uses ${var} to write out the item properties for the list item.
415
- - standard css class `joe-panel-content-option`
416
-
417
-
418
- ###c | Conditional select that changes the item schema
419
-
420
- fields:{
421
- species:{label:'Species',type:'select', values:['cat','dog','rat','thing'], onchange:adjustSchema},
422
- [field_id]:{
423
-
424
- +label : STR
425
- +type : STR
426
- value : STR (default value)
427
- +values : ARRAY/FUNC (for select)
428
-
429
- //modifiers
430
- +hidden:BOOL/STRING(name of field that toggles this) //don't show, but value is passed
431
- +locked:BOOL // show, but uneditable
432
- //events
433
- +onchange : FUNC
434
- +onblur : FUNC
435
- +onkeypress : FUNC
436
- +rerender : STRING // name of field to rerender
437
- }
438
- }
439
-
440
- function adjustSchema(dom){
441
- var species = $(dom).val();
442
- if(species == "thing"){
443
- JOE.resetSchema('thing')
444
- }
445
- else{
446
- JOE.resetSchema('animal')
447
-
448
- }
449
- }
450
-
451
- ###d | duplicating an item
452
-
453
- //duplicates the currently active object (being edited)
454
- _joe.duplicateObject(specs);
455
-
456
- **specs**
457
-
458
- - `deletes`:array of properties to clear for new item
459
- - note that you will need to delete guid/id fields or the id will be the same.
460
-
461
-
462
-
463
- ### e | exporting an object in pretty format json (or minified)
464
- JOE.exportJSON = function(object,objvarname,minify)
465
-
466
- ##Useful Functions
467
- _joe.reload(hideMessage,specs)
468
- - use specs.overwreite object to extend reloaded object.
469
-
1
+ <img src="http://joe.craydent.com/JsonObjectEditor/img/svgs/joe_banner_o.svg"/>
2
+
3
+ # Json Object Editor
4
+ JOE is software that allows you to manage data models via JSON objects. There are two flavors, the client-side version and nodejs server platform.
5
+
6
+
7
+
8
+ ## Architecture & Mental Model (Server)
9
+
10
+ - Global JOE
11
+ - Created in `server/init.js`; exposes subsystems like `JOE.Utils`, `JOE.Schemas`, `JOE.Storage`, `JOE.Mongo`/`JOE.MySQL`, `JOE.Cache`, `JOE.Apps`, `JOE.Server` (Express), `JOE.Sites`, `JOE.io` (Socket), and `JOE.auth`.
12
+ - Init pattern
13
+ - Modules can optionally export `init()`; `init.js` loads/watches modules and calls `init()` after `JOE.Server` is ready. This enables hot-reload and keeps `Server.js` thin.
14
+ - Schemas & events
15
+ - `JOE.Schemas` loads from `server/schemas/` and app schema dir into `JOE.Schemas.schema` with names in `JOE.Schemas.schemaList`. Raw copies live in `JOE.Schemas.raw_schemas` for event hooks.
16
+ - `JOE.Storage.save()` triggers schema events (`create`, `save`, `status`, `delete`) via `JOE.Schemas.events()` and writes history documents to `_history`.
17
+ - Storage & cache
18
+ - `JOE.Storage.load(collection, query, cb)` chooses backend per schema `storage.type` (`mongo`, `mysql`, `file`, `api`), with Mongo/file fallback.
19
+ - `JOE.Storage.save(item, collection, cb, { user, history })` emits `item_updated` to sockets, records history, and fires events.
20
+ - `JOE.Cache.update(cb, collections)` populates `JOE.Data`, flattens a `list`, and builds `lookup`. Use `JOE.Cache.findByID(collection, idOrCsv)` for fast lookups; `JOE.Cache.search(query)` for in-memory filtering.
21
+ - Auth
22
+ - `JOE.auth` middleware checks cookie token or Basic Auth; many API routes (and `/mcp`) are protected.
23
+ - Shorthand `$J`
24
+ - Server and client convenience: `$J.get(_id)`, `$J.search(query)`, `$J.schema(name)`.
25
+ - On server, provided by `server/modules/UniversalShorthand.js` and assigned to `global.$J` in `init.js`.
26
+ - MCP overview
27
+ - Manifest: `/.well-known/mcp/manifest.json`; JSON-RPC: `POST /mcp` (auth-protected). Tools map to real JOE APIs (`Schemas`, `Storage`, `Cache`) and sanitize sensitive fields.
28
+
29
+ ## MCP routes and local testing
30
+
31
+ - Endpoints
32
+ - Manifest (public): `GET /.well-known/mcp/manifest.json`
33
+ - JSON-RPC (auth): `POST /mcp`
34
+ - Privacy (public): `/privacy` (uses Setting `PRIVACY_CONTACT` for contact email)
35
+ - Terms (public): `/terms`
36
+
37
+ - Auth
38
+ - If users exist, `POST /mcp` requires cookie or Basic Auth (same as other APIs). If no users configured, it is effectively open.
39
+
40
+ - Test page
41
+ - JOE ships a simple tester at `/_www/mcp-test.html` inside the package.
42
+ - Access via JOE path: `http://localhost:<PORT>/JsonObjectEditor/_www/mcp-test.html`
43
+ - If your host app serves its own `_www`, the tester can also be available at the root (fallback) if running with the updated server that mounts JOE’s `_www` as a secondary static directory. Then: `http://localhost:<PORT>/mcp-test.html`
44
+
45
+ - Tools
46
+ - `listSchemas(name?)`, `getSchema(name)`
47
+ - `getObject(_id, schema?)` (supports optional `flatten` and `depth`)
48
+ - `search` (exact): unified tool for cache and storage
49
+ - Params: `{ schema?, query?, ids?, source?: 'cache'|'storage', limit?, flatten?, depth? }`
50
+ - Defaults to cache across all collections; add `schema` to filter; set `source:"storage"` to query a specific schema in the DB. Use `fuzzySearch` for typo-tolerant free text.
51
+ - `fuzzySearch` (typo-tolerant free text across weighted fields)
52
+ - Params: `{ schema?, q, filters?, fields?, threshold?, limit?, offset?, highlight?, minQueryLength? }`
53
+ - Defaults: `fields` resolved from schema `searchables` (plural) if present; otherwise weights `name:0.6, info:0.3, description:0.1`. `threshold:0.5`, `limit:50`, `minQueryLength:2`.
54
+ - Returns: `{ items, count }`. Each item may include `_score` (0..1) and `_matches` when `highlight` is true.
55
+ - `saveObject({ object })`
56
+ - `saveObjects({ objects, stopOnError?, concurrency? })`
57
+ - Batch save with per-item history/events. Defaults: `stopOnError=false`, `concurrency=5`.
58
+ - Each object must include `itemtype`; `_id` and `joeUpdated` are set when omitted.
59
+ - Return shape: `{ results, errors, saved, failed }`.
60
+
61
+ - Quick tests (PowerShell)
62
+ - Prepare headers if using Basic Auth:
63
+ ```powershell
64
+ $pair = "user:pass"; $b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
65
+ $h = @{ Authorization = "Basic $b64"; "Content-Type" = "application/json" }
66
+ $base = "http://localhost:<PORT>"
67
+ ```
68
+ - Manifest:
69
+ ```powershell
70
+ Invoke-RestMethod "$base/.well-known/mcp/manifest.json"
71
+ ```
72
+ - listSchemas:
73
+ ```powershell
74
+ $body = @{ jsonrpc="2.0"; id="1"; method="listSchemas"; params=@{} } | ConvertTo-Json -Depth 6
75
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
76
+ ```
77
+ - getSchema:
78
+ ```powershell
79
+ $body = @{ jsonrpc="2.0"; id="2"; method="getSchema"; params=@{ name="<schemaName>" } } | ConvertTo-Json -Depth 6
80
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
81
+ ```
82
+ - search (cache default):
83
+ ```powershell
84
+ $body = @{ jsonrpc="2.0"; id="3"; method="search"; params=@{ query=@{ itemtype="<schemaName>" }; limit=10 } } | ConvertTo-Json -Depth 10
85
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
86
+ ```
87
+ - fuzzySearch (cache):
88
+ ```powershell
89
+ $body = @{ jsonrpc="2.0"; id="6"; method="fuzzySearch"; params=@{ schema="<schemaName>"; q="st paal"; threshold=0.35; limit=10 } } | ConvertTo-Json -Depth 10
90
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
91
+ ```
92
+ - search (storage):
93
+ ```powershell
94
+ $body = @{ jsonrpc="2.0"; id="4"; method="search"; params=@{ schema="<schemaName>"; source="storage"; query=@{ }; limit=10 } } | ConvertTo-Json -Depth 10
95
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
96
+ ```
97
+ - search (ids + flatten):
98
+ ```powershell
99
+ $body = @{ jsonrpc="2.0"; id="5"; method="search"; params=@{ schema="<schemaName>"; ids=@("<id1>","<id2>"); flatten=$true; depth=2 } } | ConvertTo-Json -Depth 10
100
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
101
+ ```
102
+ - saveObject:
103
+ ```powershell
104
+ $object = @{ itemtype="<schemaName>"; name="Test via MCP" }
105
+ $body = @{ jsonrpc="2.0"; id="4"; method="saveObject"; params=@{ object=$object } } | ConvertTo-Json -Depth 10
106
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
107
+ ```
108
+ - saveObjects (batch):
109
+ ```powershell
110
+ $objs = @(
111
+ @{ itemtype="<schemaName>"; name="Batch A" },
112
+ @{ itemtype="<schemaName>"; name="Batch B" }
113
+ )
114
+ $body = @{ jsonrpc="2.0"; id="7"; method="saveObjects"; params=@{ objects=$objs; stopOnError=$false; concurrency=5 } } | ConvertTo-Json -Depth 10
115
+ Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
116
+ ```
117
+
118
+ - Quick tests (curl)
119
+ - Manifest:
120
+ ```bash
121
+ curl -s http://localhost:<PORT>/.well-known/mcp/manifest.json | jq
122
+ ```
123
+ - listSchemas:
124
+ - search (cache):
125
+ ```bash
126
+ curl -s -X POST http://localhost:<PORT>/mcp \
127
+ -H 'Content-Type: application/json' \
128
+ -d '{"jsonrpc":"2.0","id":"3","method":"search","params":{"query":{"itemtype":"<schemaName>"},"limit":10}}' | jq
129
+ ```
130
+ - fuzzySearch:
131
+ ```bash
132
+ curl -s -X POST http://localhost:<PORT>/mcp \
133
+ -H 'Content-Type: application/json' \
134
+ -d '{"jsonrpc":"2.0","id":"6","method":"fuzzySearch","params":{"schema":"<schemaName>","q":"st paal","threshold":0.35,"limit":10}}' | jq
135
+ ```
136
+ - search (storage):
137
+ ```bash
138
+ curl -s -X POST http://localhost:<PORT>/mcp \
139
+ -H 'Content-Type: application/json' \
140
+ -d '{"jsonrpc":"2.0","id":"4","method":"search","params":{"schema":"<schemaName>","source":"storage","query":{},"limit":10}}' | jq
141
+ ```
142
+ ```bash
143
+ curl -s -X POST http://localhost:<PORT>/mcp \
144
+ -H 'Content-Type: application/json' \
145
+ -d '{"jsonrpc":"2.0","id":"1","method":"listSchemas","params":{}}' | jq
146
+ ```
147
+ - saveObjects (batch):
148
+ ```bash
149
+ curl -s -X POST http://localhost:<PORT>/mcp \
150
+ -H 'Content-Type: application/json' \
151
+ -d '{"jsonrpc":"2.0","id":"7","method":"saveObjects","params":{"objects":[{"itemtype":"<schemaName>","name":"Batch A"}],"stopOnError":false,"concurrency":5}}' | jq
152
+ ```
153
+
154
+ - Troubleshooting
155
+ - If you see a payload like `{ originalURL: "/...", site: "no site found" }`, the request hit the Sites catch-all. Ensure MCP routes are initialized before Sites (handled by default in `server/init.js` via `MCP.init()`), and use the correct URL: `/.well-known/mcp/manifest.json` or `/mcp`.
156
+ - To update contact email on /privacy and /terms, set Setting `PRIVACY_CONTACT`.
157
+
158
+ ## SERVER/PLATFORM mode
159
+ check port 2099
160
+ /JOE/
161
+ /JsonObjectEditor/docs.html
162
+ *Should auto-open on start
163
+
164
+ Json Object Editor (Universal-esque software)
165
+ (requires connection to a mongo server for full functionality)
166
+
167
+
168
+
169
+ ### JOE server instantiation (add to entry point js file)
170
+ var joe = require('json-object-editor');
171
+ or here's a custom example
172
+ var joe = require('json-object-editor')({
173
+ name:'name_your_joe' (JOE),
174
+ joedb:'local_custom_database_name' // {{mongoURL}}' if using mongolab or remote mongodb,
175
+ port:'4099', (2099)
176
+ socketPort:'4098', (2098)
177
+ sitesPort:'4100' (2100),
178
+ clusters:1,
179
+ hostname:'server name if not localhost'(localhost)
180
+ });
181
+
182
+ ## $J (universal) Shorthand JOE
183
+ $J to access helper funtions on client and server. (callbacks are optional)
184
+ $J.get(itemId,[callback])
185
+ $J.schema(schemaname,[callback])
186
+
187
+ # Client-Side (front end only)
188
+
189
+ ## js client only instantiation
190
+ var specs = {
191
+ fields:{
192
+ species:{label:'Species',type:'select', values:['cat','dog','rat','thing'], onchange:adjustSchema},
193
+ gender:{type:'select', values:['male','female']},
194
+ legs:{label:'# of Legs',type:'int', onblur:logit},
195
+ weight:{label:' Weight (lbs)',type:'number', onblur:logit},
196
+ name:{label:' pet Name', onkeyup:logValue},
197
+ //id:{label:'ID',type:'text', locked:true},
198
+ id:{label:'ID',type:'guid'},
199
+
200
+ //example of select that takes function (function is passed item)
201
+ animalLink:{label:'Link to other animal',type:'select', values:getAnimals},
202
+ hiddenizer:{hidden:true}
203
+ },
204
+ schemas:{
205
+ animal:animalschema,
206
+ thing:thingschema
207
+ },
208
+ container:string ('body'),
209
+ compact:false,
210
+ useBackButton:true,
211
+ autosave:2000,
212
+ listSubMenu:{filters:{}},
213
+ useHashlink:true,
214
+ title:'${itemtype} | ${display}'
215
+ }
216
+ var JOE = new JsonObjectEditor(specs);
217
+ JOE.init();
218
+
219
+ ##JOE CONFIG
220
+ ##specs
221
+ - useBackButton:[false] if true, back button moves through joe panels when joe has history to go to (is open).
222
+ - useHashlink:[false], true or a template for hashlinks.
223
+ default template is '${schema_name}_${_id}'
224
+ default server tempalte is '${schema_name}/${_id}'
225
+ ##SCHEMA CONFIG
226
+ ###fields
227
+ Properties for all Fields
228
+
229
+ - `label / display`: what the field should display as
230
+ *If the field type is boolean, label controls checkbox/boolean label
231
+ - `value`: default value if not one in object
232
+ - `default`: default value for field || function(object)
233
+ - `type`: what type of field should JOE show
234
+ - `hidden`: boolean / function, value will be added (but unsees by user)
235
+ - `locked`: boolean
236
+ - `condition`: boolean
237
+ - `width`: used for layout control.
238
+ - can use pixels or percentages (as string)
239
+ - `comment`: a commentthat shows up at the beginning of the field
240
+ - `tooltip`: hover/clickable tooltip that shows up next to name
241
+
242
+ **field types:**
243
+
244
+ - `rendering`: for css html and js
245
+ - `text`: default single line text.
246
+ - autocomplete: boolean // obj of specs (template, idprop)
247
+ - values:array of possibilities
248
+ -maxlength:string
249
+ - `int`: integer field
250
+ - `number`: number (float) field
251
+ - `select`: select list.
252
+ - multiple(bool)
253
+ - values(array of objects, [{value:"",name/display:""]), can be a function
254
+ - disabled:boolean(func acceptable)
255
+ - idprop: string of prop name
256
+ - `geo`: shows a map
257
+ - *takes a string array "[lat,lon]"*
258
+ - center:[lat,lon], center of map
259
+ - zoom: zoom level (higher zooms in more)
260
+ - returns "[lat,lon]"
261
+ -`image` : shows an image and HxW as th image url is typed in.
262
+ - `multisorter` : allows arrays of objects to be selected and sorted in right bin.
263
+ - values(array of objects, [{value:"",name/display:""]), can be a function
264
+ - `content` : show content on in the editor
265
+ - run: function to be run(current_object,field_properties)
266
+ - template: html template for fillTemplate(template,current_object);
267
+ - `objectlist` : a table of objects with editable properties
268
+ - properties: array of objects|strings for the object property names
269
+ -name: value in object
270
+ -display: header in objectList
271
+ - max: integer, number or items that can be added. use zero for infinite.
272
+ - hideHeadings: don't show table headings
273
+ - `objectReference` : a list of object ids
274
+ - template
275
+ - autocomplete_template
276
+ - idprop
277
+ - values
278
+ - max(0 unlimited)
279
+ - sortable(true)
280
+ - `code` :
281
+ - language
282
+
283
+ - `boolean`:
284
+ - label:controls checkbox label
285
+ - `preview` :
286
+ -content: string or function(current joe object) to replace everything on page (template).
287
+ -bodycontent: same as content, only replaces body content.
288
+ -url: preview page if not the default one.
289
+ - encoded: boolean, if pre uriencoded
290
+ **labels:**
291
+
292
+ - pass an object instead of a string to the fields array.
293
+
294
+
295
+ {label:'Name of the following properties section'}
296
+
297
+ ##page sections
298
+ {section_start: 'SectionName',
299
+ section_label:'Section Name with Labels',
300
+ condition:function(item){
301
+ return item.show;}
302
+ },
303
+ {section_end: 'CreativeBrief'}
304
+
305
+ - pass an object instead of a string to the fields array. these show up on the details view as anchors.
306
+
307
+
308
+ - Object Properties
309
+ - `section_start`: name/id of section
310
+ - `'section_label`:use instead of section_start for display name
311
+ - `section_end`: name/id of section(str)
312
+ - template: html template for fillTemplate(template,current_object);
313
+
314
+ ##page sidebar
315
+ {sidebar_start: 'SectionName',
316
+ sidebar_label:'Section Name with Labels',
317
+ condition:function(item){
318
+ return item.show;}
319
+ },
320
+ {sidebar_end: 'CreativeBrief'}
321
+
322
+ - pass an object instead of a string to the fields array. these show up on the details view as anchors.
323
+
324
+
325
+ - Object Properties
326
+ - `sidebar_start`: name/id of sidebar
327
+ - `sidebar_label`:use instead of sidebar_start for display name
328
+ - `sidebar_end`: name/id of sidebar(str)
329
+ - template: html template for fillTemplate(template,current_object);
330
+
331
+ ###defaultProfile
332
+ overwrites the default profile
333
+
334
+ #schemas
335
+
336
+ a list of schema objects that can configure the editor fields, these can be given properties that are delegated to all the corresponding fields.
337
+
338
+ var animalschema =
339
+ {
340
+ title:'Animal', *what shows as the panel header*
341
+ fields:['id','name','legs','species','weight','color','gender','animalLink'], *list of visible fields*
342
+ _listID:'id', *the id for finding the object*
343
+ _listTitle:'${name} ${species}', *how to display items in the list*
344
+ menu:[array of menu buttons],
345
+ listMenuTitle: (string) template forjoe window title in list view,
346
+ listmenu:[array of menu buttons] (multi-edit and select all always show),
347
+ /*callback:function(obj){
348
+ alert(obj.name);
349
+ },*/
350
+ onblur:logit,
351
+
352
+ hideNumbers:boolean *toggle list numbers*
353
+ multipleCallback:function to be called after a multi-edit. passed list of edited items.
354
+ onUpdate: callback for after update. passed single edited items.
355
+ onMultipleUpdate:callback for after multi update.passed list of edited items.
356
+ filters: array of objects
357
+ checkChanges:(bool) whether or not to check for changes via interval and on leave
358
+ }
359
+ ##Table View
360
+ - add tableView object to a schema;
361
+ -cols = [strings||objects]
362
+ -string is the name and value
363
+ -display/header is the column title
364
+ -property/name = object property
365
+ ###Pre-formating
366
+ you can preformat at the joe call or schema level. The data item will be affected by the passed function (which should return the preformated item).
367
+
368
+ ##menu##
369
+ an array of menu buttons
370
+
371
+ //the default save button
372
+ //this is the dom object,
373
+ //use _joe.current.object for current object
374
+ condition:function(field,object) to call
375
+ self = Joe object
376
+ var __saveBtn__ = {name:'save',label:'Save', action:'_joe.updateObject(this);', css:'joe-save-button'};
377
+
378
+ ##itemMenu##
379
+ as array of buttons for each item in list views
380
+ - name
381
+ - action (action string)
382
+ - url (instead of js action)
383
+ - condition
384
+
385
+ ##itemExpander##
386
+ template or run for content to be shown under the main list item block.
387
+
388
+
389
+ ###Addition properties
390
+ **Changing the schema on the fly?**
391
+
392
+ _joe.resetSchema(new schema name);
393
+
394
+
395
+
396
+ **css (included) options**
397
+
398
+ - joe-left-button
399
+ - joe-right-button
400
+
401
+ ##FIELDS
402
+
403
+ {extend:'name',specs:{display:'Request Title'}},//extends the field 'name' with the specs provided.
404
+ ##usage
405
+ ### a | adding a new object
406
+
407
+ _joe.show({},{schema:'animal',callback:addAnimal);
408
+ //or goJoe(object,specs)
409
+
410
+ ...
411
+ function addAnimal(obj){
412
+ animals.push(obj);
413
+ }
414
+
415
+ ### b | viewing a list of objects
416
+
417
+ goJoe([array of objects],specs:{schema,subsets,subset})
418
+ goJoe.show(animals,{schema:'animal',subsets:[{name:'Two-Legged',filter:{legs:2}}]});
419
+ //use the specs property subset to pre-select a subset by name
420
+
421
+ **properties**
422
+
423
+ - _listWindowTitle: the title of the window (can be passed in with the schema);
424
+ - _listCount: added to the current object and can be used in the title.
425
+ - _listTitle:'${name} ${species}', *how to display items in the list*
426
+ - _icon: [str] template for a list item icon (standard min 50x50), 'http://www.icons.com/${itemname}', can be obj with width, height, url
427
+ - listSubMenu:a function or object that represents the list submenu
428
+ - stripeColor:string or function that returns valid css color descriptor.
429
+ - bgColor:string or function that returns valid css color descriptor.
430
+ - subsets: name:string, filter:object
431
+ - subMenu:a function or object that represents the single item submenu
432
+
433
+ - _listTemplate: html template that uses ${var} to write out the item properties for the list item.
434
+ - standard css class `joe-panel-content-option`
435
+
436
+ ### Helper shortcuts: subsets and stripes
437
+
438
+ Use these helpers to quickly generate subset/filter options and add per-item stripe colors.
439
+
440
+ ```javascript
441
+ // In a schema definition
442
+ {
443
+ // 1) Subsets from statuses of the current schema (auto colors)
444
+ subsets: function () {
445
+ return _joe.Filter.Options.status({
446
+ group: 'status', // optional grouping label
447
+ collapsed: true, // start group collapsed
448
+ none: true // include a "no status" option
449
+ // color: 'stripecolor' // uncomment to render colored stripes in the menu
450
+ });
451
+ },
452
+
453
+ // 2) Subsets for all distinct values of a property
454
+ // Example: recommendation_domain on 'recommendation' items
455
+ // (pass values to control order/allowlist)
456
+ // subsets: () => _joe.Filter.Options.getDatasetPropertyValues(
457
+ // 'recommendation', 'recommendation_domain', { group: 'domain', values: ['product','dietary','activity'] }
458
+ // ),
459
+
460
+ // 3) Subsets from another dataset (reference values)
461
+ // subsets: () => _joe.Filter.Options.datasetProperty('user','members',{ group: 'assignees', collapsed: true }),
462
+
463
+ // 4) Row stripe color by status (string or { color, title })
464
+ stripeColor: function (item) {
465
+ if (!item.status) return '';
466
+ const s = _joe.getDataItem(item.status, 'status');
467
+ return s && { color: s.color, title: s.name };
468
+ }
469
+ }
470
+ ```
471
+
472
+
473
+
474
+ ###c | Conditional select that changes the item schema
475
+
476
+ fields:{
477
+ species:{label:'Species',type:'select', values:['cat','dog','rat','thing'], onchange:adjustSchema},
478
+ [field_id]:{
479
+
480
+ +label : STR
481
+ +type : STR
482
+ value : STR (default value)
483
+ +values : ARRAY/FUNC (for select)
484
+
485
+ //modifiers
486
+ +hidden:BOOL/STRING(name of field that toggles this) //don't show, but value is passed
487
+ +locked:BOOL // show, but uneditable
488
+ //events
489
+ +onchange : FUNC
490
+ +onblur : FUNC
491
+ +onkeypress : FUNC
492
+ +rerender : STRING // name of field to rerender
493
+ }
494
+ }
495
+
496
+ function adjustSchema(dom){
497
+ var species = $(dom).val();
498
+ if(species == "thing"){
499
+ JOE.resetSchema('thing')
500
+ }
501
+ else{
502
+ JOE.resetSchema('animal')
503
+
504
+ }
505
+ }
506
+
507
+ ###d | duplicating an item
508
+
509
+ //duplicates the currently active object (being edited)
510
+ _joe.duplicateObject(specs);
511
+
512
+ **specs**
513
+
514
+ - `deletes`:array of properties to clear for new item
515
+ - note that you will need to delete guid/id fields or the id will be the same.
516
+
517
+
518
+
519
+ ### e | exporting an object in pretty format json (or minified)
520
+ JOE.exportJSON = function(object,objvarname,minify)
521
+
522
+ ##Useful Functions
523
+ _joe.reload(hideMessage,specs)
524
+ - use specs.overwreite object to extend reloaded object.
525
+
470
526
  _joe.constructObjectFromFields()
@@ -246,7 +246,7 @@ MCP.tools = {
246
246
  if (!object || !object.itemtype) throw new Error("'object' with 'itemtype' is required");
247
247
  const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
248
248
  // Ensure server-side update timestamp parity with /API/save
249
- if (!object.joeUpdated) { object.joeUpdated = new Date(); }
249
+ if (!object.joeUpdated) { object.joeUpdated = new Date().toISOString(); }
250
250
  // Ensure a stable _id for new objects so history and events work consistently
251
251
  try {
252
252
  if (!object._id) { object._id = (typeof cuid === 'function') ? cuid() : ($c && $c.guid ? $c.guid() : undefined); }
@@ -262,6 +262,61 @@ MCP.tools = {
262
262
  return sanitizeItems(saved)[0];
263
263
  },
264
264
 
265
+ // Batch save with bounded concurrency; preserves per-item history/events
266
+ saveObjects: async ({ objects, stopOnError = false, concurrency = 5 } = {}, ctx = {}) => {
267
+ if (!Array.isArray(objects) || objects.length === 0) {
268
+ throw new Error("'objects' (non-empty array) is required");
269
+ }
270
+ const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
271
+ const size = Math.max(1, parseInt(concurrency, 10) || 5);
272
+ const results = new Array(objects.length);
273
+ const errors = [];
274
+ let cancelled = false;
275
+
276
+ async function saveOne(obj, index){
277
+ if (cancelled) return;
278
+ if (!obj || !obj.itemtype) {
279
+ const errMsg = "Each object must include 'itemtype'";
280
+ errors.push({ index, error: errMsg });
281
+ if (stopOnError) { cancelled = true; }
282
+ return;
283
+ }
284
+ if (!obj.joeUpdated) { obj.joeUpdated = new Date().toISOString(); }
285
+ try {
286
+ if (!obj._id) {
287
+ try { obj._id = (typeof cuid === 'function') ? cuid() : ($c && $c.guid ? $c.guid() : undefined); } catch(_e) {}
288
+ }
289
+ const saved = await new Promise((resolve, reject) => {
290
+ try {
291
+ Storage.save(obj, obj.itemtype, function(err, data){
292
+ if (err) return reject(err);
293
+ resolve(data);
294
+ }, { user, history: true });
295
+ } catch (e) { reject(e); }
296
+ });
297
+ results[index] = sanitizeItems(saved)[0];
298
+ } catch (e) {
299
+ errors.push({ index, error: e && e.message ? e.message : (e+'' ) });
300
+ if (stopOnError) { cancelled = true; }
301
+ }
302
+ }
303
+
304
+ // Simple promise pool
305
+ let cursor = 0;
306
+ const runners = new Array(Math.min(size, objects.length)).fill(0).map(async function(){
307
+ while (!cancelled && cursor < objects.length) {
308
+ const idx = cursor++;
309
+ await saveOne(objects[idx], idx);
310
+ if (stopOnError && cancelled) break;
311
+ }
312
+ });
313
+ await Promise.all(runners);
314
+
315
+ const saved = results.filter(function(x){ return !!x; }).length;
316
+ const failed = errors.length;
317
+ return { results: sanitizeItems(results.filter(function(x){ return !!x; })), errors, saved, failed };
318
+ },
319
+
265
320
  // Hydration: surface core fields (from file), all schemas, statuses, and tags (no params)
266
321
  hydrate: async (_params, _ctx) => {
267
322
  let coreDef = (JOE && JOE.Fields && JOE.Fields['core']) || null;
@@ -324,6 +379,7 @@ MCP.descriptions = {
324
379
  search: "Exact search. Defaults to cache; set source=storage to query DB. Supports sortBy/sortDir, offset/limit paging, withCount, and slim response for {_id,itemtype,name,info,joeUpdated,created}. Use fuzzySearch for typo-tolerant free text.",
325
380
  fuzzySearch: "Fuzzy free‑text search. Candidates are prefiltered by 'filters' (e.g., { itemtype: 'user' }) and then scored. If 'fields' are omitted, the schema's searchables (strings) are used with best-field scoring; provide 'fields' (strings or {path,weight}) to use weighted sums. Examples: { q: 'corey hadden', filters: { itemtype: 'user' } } or { q: 'backyard', filters: { itemtype: 'house' } }.",
326
381
  saveObject: "Create/update an object; triggers events/history.",
382
+ saveObjects: "Batch save objects with bounded concurrency; per-item history/events preserved.",
327
383
  hydrate: "Loads and merges the full JOE context, including core and organization-specific schemas, relationships, universal fields (tags and statuses), and datasets. Returns a single unified object describing the active environment for use by agents, UIs, and plugins.",
328
384
  listApps: "List app definitions (title, description, collections, plugins)."
329
385
  };
@@ -408,6 +464,15 @@ MCP.params = {
408
464
  },
409
465
  required: ["object"]
410
466
  },
467
+ saveObjects: {
468
+ type: "object",
469
+ properties: {
470
+ objects: { type: "array", items: { type: "object" } },
471
+ stopOnError: { type: "boolean" },
472
+ concurrency: { type: "integer" }
473
+ },
474
+ required: ["objects"]
475
+ },
411
476
  hydrate: { type: "object", properties: {} }
412
477
  ,
413
478
  listApps: { type: "object", properties: {} }
@@ -439,6 +504,15 @@ MCP.returns = {
439
504
  },
440
505
  // When countOnly is true, search returns { count }
441
506
  saveObject: { type: "object" },
507
+ saveObjects: {
508
+ type: "object",
509
+ properties: {
510
+ results: { type: "array", items: { type: "object" } },
511
+ errors: { type: "array", items: { type: "object" } },
512
+ saved: { type: "integer" },
513
+ failed: { type: "integer" }
514
+ }
515
+ },
442
516
  hydrate: { type: "object" },
443
517
  listApps: {
444
518
  type: "object",
@@ -684,7 +684,7 @@ server.get(['/API/save/','/API/save/:itemid'],auth,function(req,res,next){
684
684
  object[prop] = $c.parseBoolean(object[prop]);
685
685
  }
686
686
  }
687
- object.joeUpdated = new Date();
687
+ object.joeUpdated = new Date().toISOString();
688
688
  //TODO: check to make sure schema idprop is present
689
689
  JOE.Storage.save(object,object.itemtype,function(err,results){
690
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