json-object-editor 0.10.657 → 0.10.662

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.
@@ -48,6 +48,9 @@ function getInclude(req,res,next){
48
48
  case 'css':
49
49
  res.set('Content-Type', 'text/css');
50
50
  break;
51
+ case 'json':
52
+ res.set('Content-Type', 'application/json');
53
+ break;
51
54
  }
52
55
  res.send(content);
53
56
  }
@@ -536,8 +539,31 @@ var routeWithoutSlash = route.startsWith('/') ? route.slice(1) : route;
536
539
  page.content = Editor.wrap(page.content,'jpe-content',{name:'page content'})
537
540
  }
538
541
 
539
- var template = layout.template.replace('${this.PAGE.content}',content.PAGE.content);
540
- var html = fillTemplate(template,content);
542
+ // First, process page content with fillTemplate to resolve variables like ${INCLUDES}, ${this.PAGE.name}, etc.
543
+ // This is needed because these variables are INSIDE the page content
544
+ var pageContent = content.PAGE.content || '';
545
+ pageContent = pageContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
546
+
547
+ // Preserve newlines before fillTemplate strips them
548
+ // Replace newlines with a temporary marker that we'll restore later
549
+ var newlineMarker = '__JOE_NEWLINE__' + Date.now() + '__';
550
+ var pageContentWithMarkers = pageContent.replace(/\n/g, newlineMarker);
551
+
552
+ // Process page content with fillTemplate to resolve template variables
553
+ var processedPageContent = fillTemplate(pageContentWithMarkers, content);
554
+
555
+ // Restore newlines after fillTemplate processing
556
+ processedPageContent = processedPageContent.replace(new RegExp(newlineMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '\n');
557
+
558
+ // Use a placeholder for ${this.PAGE.content} in layout template
559
+ var placeholder = '__JOE_PAGE_CONTENT_PLACEHOLDER__' + Date.now() + '__';
560
+ var template = layout.template.replace(/\$\{this\.PAGE\.content\}/g, placeholder);
561
+
562
+ // Process layout template with fillTemplate (which will strip newlines from layout, but our placeholder remains)
563
+ var html = fillTemplate(template, content);
564
+
565
+ // Replace placeholder with processed page content (which has newlines preserved)
566
+ html = html.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), processedPageContent);
541
567
  if(editor){
542
568
  html = html.replace('</head>',Editor.styles+'</head>')
543
569
  .replace('</body>',Editor.gui(layout)+Editor.gui(page)+Editor.menu
@@ -19,7 +19,28 @@ function ChatGPT() {
19
19
  var self = this;
20
20
  this.async ={};
21
21
  function coloredLog(message){
22
- console.log(JOE.Utils.color('[chatgpt]', 'plugin', false), message);
22
+ try{
23
+ // Only emit verbose plugin logs in non‑production environments.
24
+ // This keeps consoles clean in production while preserving rich
25
+ // traces (assistant resolution, MCP config, systemText) during
26
+ // local/dev debugging.
27
+ var env = null;
28
+ if (typeof JOE !== 'undefined' && JOE && JOE.webconfig && JOE.webconfig.env){
29
+ env = JOE.webconfig.env;
30
+ } else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV){
31
+ env = process.env.NODE_ENV;
32
+ }
33
+ if (env && env.toLowerCase() === 'production'){
34
+ return;
35
+ }
36
+ console.log(JOE.Utils.color('[chatgpt]', 'plugin', false), message);
37
+ }catch(_e){
38
+ // If anything goes wrong determining env, default to logging so
39
+ // that development debugging is not silently broken.
40
+ try{
41
+ console.log('[chatgpt]', message);
42
+ }catch(__e){}
43
+ }
23
44
  }
24
45
  //xx -setup and send a test prompt to chatgpt
25
46
  //xx get the api key from joe settings
@@ -58,6 +79,23 @@ function ChatGPT() {
58
79
  const summary = JOE.Schemas && JOE.Schemas.summary && JOE.Schemas.summary[name];
59
80
  return { full, summary };
60
81
  }
82
+ function buildMcpToolsFromConfig(cfg) {
83
+ if (!cfg || !cfg.mcp_enabled) {
84
+ return { tools: null, names: [] };
85
+ }
86
+ try {
87
+ const names = MCP.getToolNamesForToolset(
88
+ cfg.mcp_toolset || 'read-only',
89
+ Array.isArray(cfg.mcp_selected_tools) ? cfg.mcp_selected_tools : null
90
+ );
91
+ const defs = MCP.getToolDefinitions(names);
92
+ return { tools: defs, names: names };
93
+ } catch (e) {
94
+ console.warn('[chatgpt] buildMcpToolsFromConfig failed', e);
95
+ return { tools: null, names: [] };
96
+ }
97
+ }
98
+
61
99
  /**
62
100
  * callMCPTool
63
101
  *
@@ -270,29 +308,32 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
270
308
  const attachmentsMode = opts.attachments_mode || null;
271
309
  const openaiFileIds = opts.openai_file_ids || null;
272
310
 
273
- // Normalize tools: in many schemas tools may be stored as a JSON string;
274
- // here we accept either an array or a JSON-stringified array.
275
- let tools = null;
311
+ // Debug/trace: log the effective system instructions going into this
312
+ // Responses+tools call. This helps verify assistant + MCP instructions
313
+ // wiring across prompts, assists, autofill, and widget chat.
314
+ try{
315
+ coloredLog('runWithTools systemText:\n' + systemText);
316
+ }catch(_e){}
317
+
318
+ // Normalize tools: manual assistant.tools plus optional MCP tools
319
+ let manualTools = null;
276
320
  if (assistant && assistant.tools) {
277
321
  if (Array.isArray(assistant.tools)) {
278
- tools = assistant.tools;
322
+ manualTools = assistant.tools;
279
323
  } else if (typeof assistant.tools === 'string') {
280
324
  try {
281
325
  const parsed = JSON.parse(assistant.tools);
282
326
  if (Array.isArray(parsed)) {
283
- tools = parsed;
327
+ manualTools = parsed;
284
328
  }
285
329
  } catch (e) {
286
330
  console.error('[chatgpt] Failed to parse assistant.tools JSON', e);
287
331
  }
288
332
  }
289
333
  }
290
- // Normalize tool definitions for the Responses API. The assistant UI
291
- // uses the Assistants-style shape ({ type:'function', function:{...} }),
292
- // but Responses expects the name/description/parameters at the top level:
293
- // { type:'function', name:'x', description:'...', parameters:{...} }
294
- if (Array.isArray(tools)) {
295
- tools = tools.map(function (t) {
334
+ // Flatten any Assistants-style function definitions
335
+ if (Array.isArray(manualTools)) {
336
+ manualTools = manualTools.map(function (t) {
296
337
  if (t && t.type === 'function' && t.function && !t.name) {
297
338
  const fn = t.function || {};
298
339
  return {
@@ -306,6 +347,26 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
306
347
  });
307
348
  }
308
349
 
350
+ // Merge manual tools with MCP tools (manual wins on name collisions).
351
+ let tools = null;
352
+ const mergedByName = {};
353
+ const mcp = buildMcpToolsFromConfig(assistant || {});
354
+ const mcpTools = Array.isArray(mcp.tools) ? mcp.tools : null;
355
+
356
+ if (Array.isArray(manualTools) || Array.isArray(mcpTools)) {
357
+ tools = [];
358
+ (manualTools || []).forEach(function(t){
359
+ if (!t) { return; }
360
+ if (t.name) { mergedByName[t.name] = true; }
361
+ tools.push(t);
362
+ });
363
+ (mcpTools || []).forEach(function(t){
364
+ if (!t || !t.name) { return; }
365
+ if (mergedByName[t.name]) { return; }
366
+ tools.push(t);
367
+ });
368
+ }
369
+
309
370
  // No tools configured – do a simple single Responses call.
310
371
  if (!tools) {
311
372
  const resp = await openai.responses.create({
@@ -1096,6 +1157,24 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1096
1157
  return { error: "Prompt not found." };
1097
1158
  }
1098
1159
 
1160
+ // If this prompt run is associated with a JOE ai_assistant, log which
1161
+ // assistant is being used so we can debug "which agent handled this?"
1162
+ try{
1163
+ const aiAssistantId = data.ai_assistant_id || null;
1164
+ if (aiAssistantId) {
1165
+ let asst = null;
1166
+ try{
1167
+ // Prefer explicit ai_assistant schema lookup, then fallback
1168
+ // to a generic get in case datasets are flattened.
1169
+ asst = $J.get(aiAssistantId,'ai_assistant') || $J.get(aiAssistantId);
1170
+ }catch(_e){}
1171
+ const label = asst && (asst.name || asst.title || asst.info || asst._id) || aiAssistantId;
1172
+ coloredLog('[prompt] executeJOEAiPrompt using ai_assistant: '
1173
+ + label + ' [' + aiAssistantId + ']'
1174
+ + ' for prompt: ' + (prompt.name || prompt._id));
1175
+ }
1176
+ }catch(_e){}
1177
+
1099
1178
  let instructions = prompt.instructions || "";
1100
1179
  let finalInstructions=instructions;
1101
1180
  let finalInput='';
@@ -1474,9 +1553,15 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1474
1553
  system: system,
1475
1554
  messages: [],
1476
1555
  source: body.source || "widget",
1556
+ // Optional scope for object-scoped widget chats
1557
+ scope_itemtype: body.scope_itemtype || null,
1558
+ scope_id: body.scope_id || null,
1477
1559
  created: new Date().toISOString(),
1478
1560
  joeUpdated: new Date().toISOString()
1479
1561
  };
1562
+ if (body.name && !convo.name) {
1563
+ convo.name = String(body.name);
1564
+ }
1480
1565
 
1481
1566
  const saved = await new Promise(function (resolve, reject) {
1482
1567
  // Widget conversations are lightweight and do not need full history diffs.
@@ -1598,7 +1683,76 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1598
1683
  return { success: false, error: "Conversation not found" };
1599
1684
  }
1600
1685
 
1686
+ // Best-effort: if this is an object-scoped conversation and we have
1687
+ // not yet attached any files, walk the scoped object for uploader
1688
+ // style files that have OpenAI ids and cache them on the convo.
1689
+ try{
1690
+ if ((!convo.attached_openai_file_ids || !convo.attached_openai_file_ids.length) &&
1691
+ convo.scope_itemtype && convo.scope_id) {
1692
+ var scopedObj = null;
1693
+ try{
1694
+ scopedObj = $J.get(convo.scope_id, convo.scope_itemtype) || $J.get(convo.scope_id);
1695
+ }catch(_e){}
1696
+ if (scopedObj && typeof scopedObj === 'object') {
1697
+ var ids = [];
1698
+ var meta = [];
1699
+ Object.keys(scopedObj).forEach(function(field){
1700
+ var val = scopedObj[field];
1701
+ if (!Array.isArray(val)) { return; }
1702
+ val.forEach(function(f){
1703
+ if (f && f.openai_file_id) {
1704
+ ids.push(f.openai_file_id);
1705
+ meta.push({
1706
+ itemtype: scopedObj.itemtype || convo.scope_itemtype,
1707
+ field: field,
1708
+ name: f.filename || '',
1709
+ role: f.file_role || null,
1710
+ openai_file_id: f.openai_file_id
1711
+ });
1712
+ }
1713
+ });
1714
+ });
1715
+ if (ids.length) {
1716
+ convo.attached_openai_file_ids = ids;
1717
+ convo.attached_files_meta = meta;
1718
+ }
1719
+ }
1720
+ }
1721
+ }catch(_e){ /* non-fatal */ }
1722
+
1601
1723
  convo.messages = normalizeMessages(convo.messages);
1724
+
1725
+ // On the very first turn of an object-scoped widget conversation,
1726
+ // pre-load a slimmed understandObject snapshot so the assistant
1727
+ // immediately knows which record "this client/task/..." refers to
1728
+ // without having to remember to call MCP. We keep this snapshot
1729
+ // concise via slimUnderstandObjectResult and only inject it once.
1730
+ try{
1731
+ var isObjectChat = (convo.source === 'object_chat') && convo.scope_id;
1732
+ var hasMessages = Array.isArray(convo.messages) && convo.messages.length > 0;
1733
+ if (isObjectChat && !hasMessages){
1734
+ const uo = await callMCPTool('understandObject', {
1735
+ _id: convo.scope_id,
1736
+ itemtype: convo.scope_itemtype || undefined,
1737
+ depth: 1,
1738
+ slim: true
1739
+ }, { req });
1740
+ const slimmed = slimUnderstandObjectResult(uo);
1741
+ if (slimmed) {
1742
+ convo.messages = convo.messages || [];
1743
+ convo.messages.push({
1744
+ role: 'system',
1745
+ content: JSON.stringify({
1746
+ tool: 'understandObject',
1747
+ scope_object: slimmed
1748
+ })
1749
+ });
1750
+ }
1751
+ }
1752
+ }catch(_e){
1753
+ console.warn('[chatgpt] widgetMessage understandObject preload failed', _e && _e.message || _e);
1754
+ }
1755
+
1602
1756
  const nowIso = new Date().toISOString();
1603
1757
 
1604
1758
  // Append user message
@@ -1630,37 +1784,163 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1630
1784
  }
1631
1785
  }
1632
1786
 
1633
- const assistantId = body.assistant_id || convo.assistant_id || null;
1634
- // NOTE: assistantId here is the OpenAI assistant_id, not the JOE cuid.
1635
- // We do NOT pass assistant_id to the Responses API (it is not supported in the
1636
- // version we are using); instead we look up the JOE ai_assistant by assistant_id
1637
- // and inject its configuration (model, instructions, tools) into the request.
1638
- var assistantObj = null;
1639
- if (assistantId && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
1640
- assistantObj = JOE.Data.ai_assistant.find(function (a) {
1641
- return a && a.assistant_id === assistantId;
1787
+ // Resolve the JOE ai_assistant driving this conversation. We support
1788
+ // both the modern flow (ai_assistant_id / convo.assistant, which are
1789
+ // JOE cuid references) and the legacy OpenAI Assistants flow
1790
+ // (assistant_id / convo.assistant_id, which are OpenAI ids).
1791
+ var assistantObj = null;
1792
+ var joeAssistantId = body.ai_assistant_id || convo.assistant || null; // JOE cuid
1793
+ if (joeAssistantId) {
1794
+ try{
1795
+ // Prefer a direct lookup via the ai_assistant schema, but fall
1796
+ // back to scanning the in-memory dataset if needed. In some
1797
+ // server contexts $J.get may not be wired for ai_assistant yet,
1798
+ // while JOE.Data.ai_assistant is available.
1799
+ assistantObj = $J.get(joeAssistantId,'ai_assistant') || $J.get(joeAssistantId) || null;
1800
+ }catch(_e){}
1801
+ if (!assistantObj && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
1802
+ assistantObj = JOE.Data.ai_assistant.find(function(a){
1803
+ return a && (a._id === joeAssistantId);
1642
1804
  }) || null;
1643
1805
  }
1806
+ }
1807
+ const assistantId = body.assistant_id || convo.assistant_id || (assistantObj && assistantObj.assistant_id) || null;
1808
+ // Legacy fallback: if we only have an OpenAI assistant_id, try to
1809
+ // locate the JOE ai_assistant by that id.
1810
+ if (!assistantObj && assistantId && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
1811
+ assistantObj = JOE.Data.ai_assistant.find(function (a) {
1812
+ return a && a.assistant_id === assistantId;
1813
+ }) || null;
1814
+ }
1815
+
1816
+ // Log which ai_assistant (if any) is being used for this widget
1817
+ // conversation so we can easily confirm the active agent when
1818
+ // debugging object chat vs AI Hub behavior.
1819
+ try{
1820
+ coloredLog('[widget] assistant resolution: '
1821
+ + 'convo.assistant=' + String(convo.assistant || '')
1822
+ + ' convo.assistant_id=' + String(convo.assistant_id || '')
1823
+ + ' body.ai_assistant_id=' + String(body.ai_assistant_id || '')
1824
+ + ' body.assistant_id=' + String(body.assistant_id || '')
1825
+ + ' resolvedJoe=' + String(assistantObj && assistantObj._id || ''));
1826
+ if (assistantObj) {
1827
+ var asstLabel = assistantObj.name || assistantObj.title || assistantObj.info || assistantObj._id || assistantId;
1828
+ coloredLog('[widget] widgetMessage using ai_assistant: '
1829
+ + asstLabel + ' [' + (assistantObj._id || assistantId || '') + ']'
1830
+ + ' source=' + String(convo.source || 'widget')
1831
+ + ' convo=' + String(convo._id || ''));
1832
+ } else if (assistantId) {
1833
+ coloredLog('[widget] widgetMessage assistant_id (no JOE ai_assistant found): '
1834
+ + assistantId
1835
+ + ' source=' + String(convo.source || 'widget')
1836
+ + ' convo=' + String(convo._id || ''));
1837
+ }
1838
+ }catch(_e){}
1839
+
1644
1840
  const openai = newClient();
1645
1841
  const model = (assistantObj && assistantObj.ai_model) || convo.model || body.model || "gpt-5.1";
1646
1842
 
1647
1843
  // Prefer explicit system text on the conversation, then assistant instructions.
1648
- const systemText = (convo.system && String(convo.system)) ||
1844
+ const baseSystemText = (convo.system && String(convo.system)) ||
1649
1845
  (assistantObj && assistantObj.instructions) ||
1650
1846
  "";
1847
+
1848
+ // When this conversation was launched from an object ("Start Chat"
1849
+ // on a record), include a small scope hint so the assistant knows
1850
+ // which object id/itemtype to use with MCP tools like
1851
+ // understandObject/search. We keep this concise to avoid
1852
+ // unnecessary tokens but still make the scope unambiguous.
1853
+ let systemText = baseSystemText;
1854
+ try{
1855
+ if (convo.source === 'object_chat' && convo.scope_id) {
1856
+ const scopeLine = '\n\n---\nJOE scope_object:\n'
1857
+ + '- itemtype: ' + String(convo.scope_itemtype || 'unknown') + '\n'
1858
+ + '- _id: ' + String(convo.scope_id) + '\n'
1859
+ + 'When you need this object\'s details, call the MCP tool "understandObject" '
1860
+ + 'with these identifiers, or search for related records using the MCP search tools.\n';
1861
+ systemText = (baseSystemText || '') + scopeLine;
1862
+ }
1863
+ }catch(_e){ /* non-fatal */ }
1864
+
1865
+ // Append MCP tool instructions for assistants that have MCP enabled,
1866
+ // using the same helper as other MCP-aware surfaces.
1867
+ try{
1868
+ if (assistantObj && assistantObj.mcp_enabled) {
1869
+ const mcpCfg = {
1870
+ mcp_enabled: assistantObj.mcp_enabled,
1871
+ mcp_toolset: assistantObj.mcp_toolset,
1872
+ mcp_selected_tools: assistantObj.mcp_selected_tools,
1873
+ mcp_instructions_mode: assistantObj.mcp_instructions_mode || 'auto'
1874
+ };
1875
+ const mcp = buildMcpToolsFromConfig(mcpCfg);
1876
+ coloredLog('[widget] MCP config for assistant '
1877
+ + String(assistantObj._id || '') + ': enabled=' + String(mcpCfg.mcp_enabled)
1878
+ + ' toolset=' + String(mcpCfg.mcp_toolset || '')
1879
+ + ' names=' + JSON.stringify(mcp.names || []));
1880
+ if (mcp.names && mcp.names.length) {
1881
+ const txt = MCP.buildToolInstructions(mcp.names, mcpCfg.mcp_instructions_mode || 'auto');
1882
+ if (txt) {
1883
+ coloredLog('[widget] appending MCP instructions block to systemText');
1884
+ systemText = (systemText || '') + '\n\n' + txt;
1885
+ }
1886
+ }
1887
+ } else {
1888
+ coloredLog('[widget] MCP disabled for this assistant or assistant missing; no MCP block appended.');
1889
+ }
1890
+ }catch(_e){ /* non-fatal */ }
1891
+
1892
+ // Build the messages array for the model. We deliberately separate
1893
+ // the stored `convo.messages` from the model-facing payload so we
1894
+ // can annotate the latest user turn with uploaded_files metadata
1895
+ // without altering the persisted history.
1651
1896
  const messagesForModel = convo.messages.map(function (m) {
1652
1897
  return { role: m.role, content: m.content };
1653
1898
  });
1899
+ // If we have attached file metadata, wrap the latest user turn in a
1900
+ // small JSON envelope so the model can see which files exist and how
1901
+ // they are labeled (role, name, origin field) while still receiving
1902
+ // the raw user input as `input`.
1903
+ try{
1904
+ if (convo.attached_files_meta && convo.attached_files_meta.length && messagesForModel.length) {
1905
+ var lastMsg = messagesForModel[messagesForModel.length - 1];
1906
+ if (lastMsg && lastMsg.role === role && typeof lastMsg.content === 'string') {
1907
+ lastMsg.content = JSON.stringify({
1908
+ uploaded_files: convo.attached_files_meta,
1909
+ input: lastMsg.content
1910
+ }, null, 2);
1911
+ }
1912
+ }
1913
+ }catch(_e){ /* non-fatal */ }
1914
+
1915
+ // Collect OpenAI file ids from scoped object attachments and any
1916
+ // assistant-level files so they are available to the model via the
1917
+ // shared attachFilesToResponsesPayload helper inside runWithTools.
1918
+ var openaiFileIds = [];
1919
+ if (Array.isArray(convo.attached_openai_file_ids) && convo.attached_openai_file_ids.length){
1920
+ openaiFileIds = openaiFileIds.concat(convo.attached_openai_file_ids);
1921
+ }
1922
+ try{
1923
+ if (assistantObj && Array.isArray(assistantObj.assistant_files)) {
1924
+ assistantObj.assistant_files.forEach(function(f){
1925
+ if (f && f.openai_file_id) {
1926
+ openaiFileIds.push(f.openai_file_id);
1927
+ }
1928
+ });
1929
+ }
1930
+ }catch(_e){}
1654
1931
 
1655
1932
  // Use runWithTools so that, when an assistant has tools configured,
1656
- // we let the model call those tools via MCP before generating a
1657
- // final response.
1933
+ // we let the model call those tools via MCP / function tools before
1934
+ // generating a final response. Attach any discovered OpenAI files
1935
+ // so the model can read from them as needed.
1658
1936
  const runResult = await runWithTools({
1659
1937
  openai: openai,
1660
1938
  model: model,
1661
1939
  systemText: systemText,
1662
1940
  messages: messagesForModel,
1663
1941
  assistant: assistantObj,
1942
+ attachments_mode: (body.attachments_mode || 'direct'),
1943
+ openai_file_ids: openaiFileIds.length ? openaiFileIds : null,
1664
1944
  req: req
1665
1945
  });
1666
1946
 
@@ -335,6 +335,104 @@ function FormBuilder(){
335
335
  }
336
336
  }//end bodycontent
337
337
 
338
+ /**
339
+ * Get form definition JSON from an include
340
+ * GET /API/plugin/formBuilder/definition?include_id={id}
341
+ * GET /API/plugin/formBuilder/definition?formid={form_id}&field=json_definition_include
342
+ * GET /API/plugin/formBuilder/definition?formid={form_id}&pageid={page_id} (auto-finds JSON from page includes)
343
+ */
344
+ this.definition = function(data, req, res) {
345
+ var includeId = data.include_id;
346
+ var formId = data.formid;
347
+ var pageId = data.pageid;
348
+ var includeField = data.field || 'json_definition_include';
349
+
350
+ // If formid provided, look up the include from form's meta, page includes, or specified field
351
+ if (formId && !includeId) {
352
+ var form = JOE.Cache.findByID('form', formId);
353
+ if (!form) {
354
+ return res.status(404).json({errors: 'form not found', failedat: 'formBuilder'});
355
+ }
356
+
357
+ // First try: Check if form has a reference to include in meta or a direct field
358
+ if (form.meta && form.meta[includeField]) {
359
+ includeId = form.meta[includeField];
360
+ } else if (form[includeField]) {
361
+ includeId = form[includeField];
362
+ }
363
+ // Second try: If pageid provided, find JSON include from page's includes
364
+ else if (pageId) {
365
+ var page = JOE.Cache.findByID('page', pageId);
366
+ if (page && page.includes && Array.isArray(page.includes)) {
367
+ for (var i = 0; i < page.includes.length; i++) {
368
+ var inc = JOE.Cache.findByID('include', page.includes[i]);
369
+ if (inc && inc.filetype === 'json') {
370
+ includeId = inc._id;
371
+ break;
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ if (!includeId) {
378
+ return res.status(404).json({errors: 'form does not have json definition include reference. Set it in form.meta.json_definition_include or add a JSON include to the page', failedat: 'formBuilder'});
379
+ }
380
+ }
381
+
382
+ if (!includeId) {
383
+ return res.status(400).json({errors: 'include_id or formid required', failedat: 'formBuilder'});
384
+ }
385
+
386
+ var include = JOE.Cache.findByID('include', includeId);
387
+ if (!include) {
388
+ return res.status(404).json({errors: 'include not found', failedat: 'formBuilder'});
389
+ }
390
+
391
+ if (include.filetype !== 'json') {
392
+ return res.status(400).json({errors: 'include is not a JSON file', failedat: 'formBuilder'});
393
+ }
394
+
395
+ var content = include.content;
396
+
397
+ // If fillTemplate is enabled, process template variables
398
+ if (include.fillTemplate) {
399
+ var payload = {
400
+ WEBCONFIG: JOE.webconfig,
401
+ SETTING: JOE.Cache.settings,
402
+ REQUEST: req,
403
+ FORM: formId ? JOE.Cache.findByID('form', formId) : null
404
+ };
405
+ try {
406
+ var fillTemplate = require('../modules/Sites.js').fillTemplate || function(str, data) {
407
+ return str.replace(/\$\{this\.([^}]+)\}/g, function(match, path) {
408
+ var parts = path.split('.');
409
+ var val = data;
410
+ for (var i = 0; i < parts.length; i++) {
411
+ if (val && typeof val === 'object') {
412
+ val = val[parts[i]];
413
+ } else {
414
+ return match;
415
+ }
416
+ }
417
+ return val != null ? val : match;
418
+ });
419
+ };
420
+ content = fillTemplate(content, payload);
421
+ } catch(e) {
422
+ console.error('[formBuilder.definition] template fill error:', e);
423
+ }
424
+ }
425
+
426
+ // Parse and return JSON
427
+ try {
428
+ var jsonData = typeof content === 'string' ? JSON.parse(content) : content;
429
+ res.set('Content-Type', 'application/json');
430
+ return res.json(jsonData);
431
+ } catch(e) {
432
+ return res.status(500).json({errors: 'invalid JSON: ' + e.message, failedat: 'formBuilder'});
433
+ }
434
+ };
435
+
338
436
  return self;
339
437
  }
340
438
  module.exports = new FormBuilder();