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 +6 -0
- package/_www/mcp-export.html +4 -3
- package/_www/mcp-nav.js +7 -4
- package/_www/mcp-test.html +16 -0
- package/package.json +1 -1
- package/readme.md +525 -469
- package/server/modules/MCP.js +75 -1
- package/server/modules/Server.js +1 -1
- package/server/modules/Storage.js +9 -0
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)
|
package/_www/mcp-export.html
CHANGED
|
@@ -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;
|
|
5
|
-
nav
|
|
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(){
|
package/_www/mcp-test.html
CHANGED
|
@@ -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
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
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
-
|
|
69
|
-
```powershell
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
- `
|
|
232
|
-
- `
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
- `
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
- `
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
- `
|
|
265
|
-
|
|
266
|
-
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
- `
|
|
310
|
-
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
##
|
|
360
|
-
|
|
361
|
-
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
##
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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()
|
package/server/modules/MCP.js
CHANGED
|
@@ -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",
|
package/server/modules/Server.js
CHANGED
|
@@ -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
|
|