jsgui3-server 0.0.151 → 0.0.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +21 -0
  2. package/admin-ui/v1/controls/admin_shell.js +33 -0
  3. package/admin-ui/v1/server.js +14 -1
  4. package/docs/agi/skills/README.md +23 -0
  5. package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
  6. package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
  7. package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
  8. package/docs/agi/skills/deep-research/SKILL.md +156 -0
  9. package/docs/agi/skills/endurance/SKILL.md +53 -0
  10. package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
  11. package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
  12. package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
  13. package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
  14. package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
  15. package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
  16. package/docs/agi/skills/session-discipline/SKILL.md +40 -0
  17. package/docs/agi/skills/skill-writing/SKILL.md +211 -0
  18. package/docs/agi/skills/static-analysis/SKILL.md +58 -0
  19. package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
  20. package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
  21. package/docs/api-reference.md +120 -2
  22. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
  23. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
  24. package/docs/books/website-design/01-introduction.md +73 -0
  25. package/docs/books/website-design/02-current-state.md +195 -0
  26. package/docs/books/website-design/03-base-class.md +181 -0
  27. package/docs/books/website-design/04-webpage.md +307 -0
  28. package/docs/books/website-design/05-website.md +456 -0
  29. package/docs/books/website-design/06-pages-storage.md +170 -0
  30. package/docs/books/website-design/07-api-layer.md +285 -0
  31. package/docs/books/website-design/08-server-integration.md +271 -0
  32. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  33. package/docs/books/website-design/10-open-questions.md +196 -0
  34. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  35. package/docs/books/website-design/12-content-model.md +395 -0
  36. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  37. package/docs/books/website-design/14-website-module-spec.md +541 -0
  38. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  39. package/docs/books/website-design/16-minimal-first.md +203 -0
  40. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  41. package/docs/books/website-design/README.md +43 -0
  42. package/docs/bundling-system-deep-dive.md +112 -3
  43. package/docs/configuration-reference.md +84 -0
  44. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  45. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  46. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  47. package/docs/swagger.md +316 -0
  48. package/examples/controls/1) window/server.js +6 -1
  49. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  50. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  51. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  52. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  53. declarative api/e2e-screenshot-1-name-change.png +0 -0
  54. declarative api/e2e-screenshot-2-toggled.png +0 -0
  55. declarative api/e2e-screenshot-3-final.png +0 -0
  56. declarative api/e2e-screenshot-final.png +0 -0
  57. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  58. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  59. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  60. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  61. package/examples/data-views/01) query-endpoint/server.js +61 -0
  62. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  63. package/labs/website-design/002-pages-storage/check.js +244 -0
  64. package/labs/website-design/002-pages-storage/results.txt +0 -0
  65. package/labs/website-design/003-type-detection/check.js +193 -0
  66. package/labs/website-design/003-type-detection/results.txt +0 -0
  67. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  68. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  69. package/labs/website-design/005-normalize-input/check.js +303 -0
  70. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  71. package/labs/website-design/README.md +34 -0
  72. package/labs/website-design/manifest.json +68 -0
  73. package/labs/website-design/run-all.js +60 -0
  74. package/middleware/json-body.js +126 -0
  75. package/openapi.js +474 -0
  76. package/package.json +13 -7
  77. package/publishers/Publishers.js +6 -5
  78. package/publishers/http-function-publisher.js +135 -126
  79. package/publishers/http-webpage-publisher.js +89 -11
  80. package/publishers/query-publisher.js +116 -0
  81. package/publishers/swagger-publisher.js +203 -0
  82. package/publishers/swagger-ui.js +578 -0
  83. package/resources/adapters/array-adapter.js +143 -0
  84. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
  85. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
  86. package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
  87. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
  88. package/resources/query-resource.js +131 -0
  89. package/serve-factory.js +677 -18
  90. package/server.js +585 -167
  91. package/tests/README.md +86 -2
  92. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  93. package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
  94. package/tests/control-elimination-root-feature-pruning.test.js +440 -0
  95. package/tests/control-elimination-static-bracket-access.test.js +245 -0
  96. package/tests/control-scan-manifest-regression.test.js +2 -0
  97. package/tests/end-to-end.test.js +22 -21
  98. package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
  99. package/tests/helpers/playwright-e2e-harness.js +326 -0
  100. package/tests/helpers/puppeteer-e2e-harness.js +62 -1
  101. package/tests/openapi.test.js +319 -0
  102. package/tests/playwright-smoke.test.js +134 -0
  103. package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
  104. package/tests/publish-enhancements.test.js +673 -0
  105. package/tests/query-publisher.test.js +430 -0
  106. package/tests/quick-json-body-test.js +169 -0
  107. package/tests/serve.test.js +425 -122
  108. package/tests/swagger-publisher.test.js +1076 -0
  109. package/tests/test-runner.js +4 -0
@@ -29,7 +29,51 @@ const {
29
29
 
30
30
 
31
31
 
32
+ /**
33
+ * Function_Publisher — publishes a JavaScript function as an HTTP API endpoint.
34
+ *
35
+ * Wraps a plain function (sync or async) so it can be called over HTTP.
36
+ * Incoming JSON request bodies are parsed and passed as the function's
37
+ * first argument. Return values are JSON-serialised back to the client.
38
+ *
39
+ * ## Metadata for OpenAPI / Swagger
40
+ *
41
+ * When constructed via `server.publish()`, a `meta` object may be
42
+ * attached to the spec. The publisher stores this as `this.api_meta`
43
+ * and the OpenAPI generator reads it to produce Swagger documentation.
44
+ *
45
+ * Supported `meta` / shorthand fields:
46
+ *
47
+ * | Field | Type | Purpose |
48
+ * |----------------|----------|------------------------------------------|
49
+ * | `method` | string | HTTP method (`'GET'`, `'POST'`, etc.) |
50
+ * | `summary` | string | One-line summary for Swagger UI |
51
+ * | `description` | string | Multi-line Markdown description |
52
+ * | `tags` | string[] | Grouping tags in the Swagger UI |
53
+ * | `params` | Object | Request body schema (simple format) |
54
+ * | `returns` | Object | Response body schema (simple format) |
55
+ * | `deprecated` | boolean | Mark endpoint as deprecated |
56
+ * | `operationId` | string | Custom OpenAPI operation ID |
57
+ *
58
+ * @extends HTTP_Publisher
59
+ * @see {@link module:openapi} for the spec generator that reads `api_meta`.
60
+ */
32
61
  class Function_Publisher extends HTTP_Publisher {
62
+ /**
63
+ * Create a new Function_Publisher.
64
+ *
65
+ * @param {Function|Object} spec - Either a bare function or an options object.
66
+ * @param {Function} spec.fn - The function to publish.
67
+ * @param {string} [spec.name] - Endpoint name (used in route).
68
+ * @param {Object} [spec.schema] - Schema for introspection.
69
+ * @param {Object} [spec.meta] - API metadata for OpenAPI generation.
70
+ * @param {string} [spec.summary] - Shorthand for `meta.summary`.
71
+ * @param {string} [spec.description] - Shorthand for `meta.description`.
72
+ * @param {string[]} [spec.tags] - Shorthand for `meta.tags`.
73
+ * @param {Object} [spec.params] - Shorthand for `meta.params`.
74
+ * @param {Object} [spec.returns] - Shorthand for `meta.returns`.
75
+ * @param {string} [spec.method] - Shorthand for `meta.method`.
76
+ */
33
77
  constructor(spec) {
34
78
  super(spec);
35
79
  //let fn = this.fn = spec;
@@ -46,6 +90,29 @@ class Function_Publisher extends HTTP_Publisher {
46
90
  } else {
47
91
  this.schema = {};
48
92
  }
93
+
94
+ // ── Extended API metadata for OpenAPI / Swagger generation ──
95
+ //
96
+ // Metadata can be supplied in two ways:
97
+ // 1. Nested under `spec.meta` (from server.publish())
98
+ // 2. As top-level shorthand fields on the spec itself
99
+ //
100
+ // Both are merged into `this.api_meta`, with `spec.meta`
101
+ // taking precedence over shorthand fields.
102
+ //
103
+ // The OpenAPI generator (openapi.js) reads `this.api_meta`
104
+ // to produce requestBody, responses, tags, summary, etc.
105
+ this.api_meta = {};
106
+ if (spec.meta && typeof spec.meta === 'object') {
107
+ this.api_meta = { ...spec.meta };
108
+ }
109
+ // Merge top-level shorthand fields (lower precedence).
110
+ if (spec.summary) this.api_meta.summary = this.api_meta.summary || spec.summary;
111
+ if (spec.description) this.api_meta.description = this.api_meta.description || spec.description;
112
+ if (spec.tags) this.api_meta.tags = this.api_meta.tags || spec.tags;
113
+ if (spec.params) this.api_meta.params = this.api_meta.params || spec.params;
114
+ if (spec.returns) this.api_meta.returns = this.api_meta.returns || spec.returns;
115
+ if (spec.method) this.api_meta.method = this.api_meta.method || spec.method;
49
116
  }
50
117
  this.type = 'function';
51
118
  //let fn = spec;
@@ -54,33 +121,18 @@ class Function_Publisher extends HTTP_Publisher {
54
121
  // But will need to route to the function publisher.
55
122
 
56
123
  this.handle_http = (req, res) => {
57
- // need to handle observable http request.
58
- // Begin sending to that connection...
59
- // Following SSE would be nice.
60
-
61
- // Should check to see if it supports compression.
62
- // Compression function middleware could work fine here.
63
-
64
- // Will need to call the function with params.
65
- // Read params from content body?
66
-
67
- const {method, headers} = req;
68
-
69
- //console.log('Function Publisher handle_http method', method);
70
- //console.log('headers', headers);
71
-
72
- // Need to get the incoming parameters.
73
- // Need to use formidable or whatever else...?
74
-
75
- // Need to wait for the whole of the request to complete.
76
- // Don't think it will be multipart forms.
124
+ const { method, headers } = req;
125
+
126
+ // ── Parse query string from the URL ──
127
+ let query_params = {};
128
+ try {
129
+ const parsed_url = new URL(req.url, `http://${headers.host || 'localhost'}`);
130
+ query_params = Object.fromEntries(parsed_url.searchParams.entries());
131
+ } catch (e) {
132
+ // Fallback: no query params if URL parsing fails.
133
+ }
77
134
 
78
- const content_length = headers['content-length'];
79
135
  const content_type = headers['content-type'];
80
- // could be the mime type and the charset.
81
-
82
- // then need to wait for the whole thing.
83
- // think the req is a Readable_Stream.
84
136
 
85
137
  const chunks = [];
86
138
 
@@ -90,77 +142,76 @@ class Function_Publisher extends HTTP_Publisher {
90
142
 
91
143
  req.on('end', () => {
92
144
  const buf_input = Buffer.concat(chunks);
93
- //console.log('buf_input', buf_input);
94
-
95
- // then interpret it according to the content_type
96
- let obj_input;
97
- //console.log('content_type', content_type);
98
- if (!content_type) {
99
- console.log('buf_input.length', buf_input.length);
100
- if (buf_input.length === 0) {
101
-
102
- } else {
103
- console.trace();
104
- throw 'NYI';
105
- }
106
- } else {
107
- if (content_type.startsWith('text/plain')) {
108
- obj_input = buf_input.toString();
109
-
110
- } else {
111
145
 
112
- if (content_type === 'application/json') {
146
+ // ── Parse request body ──
147
+ let body_input = null;
148
+ if (buf_input.length > 0) {
149
+ if (!content_type) {
150
+ // No content-type but has body — try JSON parse.
151
+ try {
152
+ body_input = JSON.parse(buf_input.toString());
153
+ } catch (e) {
154
+ body_input = buf_input.toString();
155
+ }
156
+ } else if (content_type.startsWith('text/plain')) {
157
+ body_input = buf_input.toString();
158
+ } else if (content_type === 'application/json' || content_type.startsWith('application/json')) {
113
159
  const inputStr = buf_input.toString();
114
- if (inputStr.trim() === '') {
115
- obj_input = null;
116
- } else {
117
- obj_input = JSON.parse(inputStr);
160
+ if (inputStr.trim() !== '') {
161
+ body_input = JSON.parse(inputStr);
118
162
  }
119
163
  } else {
120
- console.trace();
121
- throw 'NYI';
164
+ // Unknown content type — try JSON, fall back to string.
165
+ try {
166
+ body_input = JSON.parse(buf_input.toString());
167
+ } catch (e) {
168
+ body_input = buf_input.toString();
169
+ }
122
170
  }
123
- // decode / parse JSON.
124
171
  }
125
- }
126
-
127
-
128
172
 
173
+ // ── Merge inputs: path params < query params < body ──
174
+ // Priority (highest wins): body > query > path params
175
+ const path_params = (req.params && typeof req.params === 'object') ? req.params : {};
176
+ const has_query = Object.keys(query_params).length > 0;
177
+ const has_path = Object.keys(path_params).length > 0;
178
+ const has_body = body_input !== null && body_input !== undefined;
179
+
180
+ let merged_input;
181
+ if (has_body && typeof body_input === 'object' && !Array.isArray(body_input)) {
182
+ // Body is an object — merge with path + query params.
183
+ merged_input = { ...path_params, ...query_params, ...body_input };
184
+ } else if (has_body) {
185
+ // Body is a string or array — use as-is if no path/query params.
186
+ if (has_path || has_query) {
187
+ merged_input = { ...path_params, ...query_params, _body: body_input };
188
+ } else {
189
+ merged_input = body_input;
190
+ }
191
+ } else if (has_query || has_path) {
192
+ // No body — use path + query params as input.
193
+ merged_input = { ...path_params, ...query_params };
194
+ } else {
195
+ // Nothing at all.
196
+ merged_input = undefined;
197
+ }
129
198
 
130
199
  const output_all = (call_res) => {
131
- // But not a buffer, string???
132
- // res is response.
133
200
  const tcr = tf(call_res);
134
-
135
- console.log('tcr', tcr);
136
-
137
201
  res.writeHead(200, {
138
- 'Content-Type': 'application/json'//,
139
- //'Transfer-Encoding': 'chunked',
140
- //'Trailer': 'Content-MD5'
202
+ 'Content-Type': 'application/json'
141
203
  });
142
204
  res.end(JSON.stringify(call_res));
143
205
  }
144
206
 
145
-
146
- // And the function to call may be async.
147
- // Can test to see if we get a promise (or observable) back from it.
148
-
149
207
  try {
150
- const fn_res = fn(obj_input);
208
+ const fn_res = fn(merged_input);
151
209
  const tfr = tf(fn_res);
152
- //console.log('fn_res', fn_res);
153
-
154
- //
155
210
 
156
211
  if (tfr === 'p') {
157
212
  // promise
158
- console.log('need to await promise resolution');
159
-
160
213
  fn_res.then(call_res => {
161
- console.log('fn_res then happened, call_res', call_res);
162
214
  output_all(call_res);
163
-
164
215
  }, err => {
165
216
  console.error('Function execution error:', err);
166
217
  if (!res.headersSent) {
@@ -170,36 +221,22 @@ class Function_Publisher extends HTTP_Publisher {
170
221
  res.end(JSON.stringify({ error: err.message }));
171
222
  }
172
223
  });
173
-
174
-
175
224
  } else if (tfr === 's') {
176
- // Just write it as a string for the moment I think?
177
- // Or always encode as JSON?
178
-
179
- // text/plain;charset=UTF-8
180
-
181
225
  res.writeHead(200, {
182
- 'Content-Type': 'text/plain;charset=UTF-8'//,
183
- //'Transfer-Encoding': 'chunked',
184
- //'Trailer': 'Content-MD5'
226
+ 'Content-Type': 'text/plain;charset=UTF-8'
185
227
  });
186
228
  res.end(fn_res);
187
-
188
-
189
229
  } else if (tfr === 'o' || tfr === 'a') {
190
- // Just write it as a string for the moment I think?
191
- // Or always encode as JSON?
192
-
193
- // text/plain;charset=UTF-8
194
-
195
230
  res.writeHead(200, {
196
- 'Content-Type': 'application/json'//,
197
- //'Transfer-Encoding': 'chunked',
198
- //'Trailer': 'Content-MD5'
231
+ 'Content-Type': 'application/json'
199
232
  });
200
233
  res.end(JSON.stringify(fn_res));
201
-
202
-
234
+ } else if (tfr === 'u' || tfr === 'n') {
235
+ // undefined or null return — send empty 200
236
+ res.writeHead(200, {
237
+ 'Content-Type': 'application/json'
238
+ });
239
+ res.end('null');
203
240
  } else {
204
241
  console.log('tfr', tfr);
205
242
  console.trace();
@@ -214,43 +251,15 @@ class Function_Publisher extends HTTP_Publisher {
214
251
  res.end(JSON.stringify({ error: err.message }));
215
252
  }
216
253
  }
217
-
218
-
219
-
220
-
221
-
222
- // turn it to string?
223
- // check its mime type?
224
-
225
- // however, likely we should have been told its application/json if that's the case.
226
- // likely will be the case for function calls.
227
-
228
- // the jsgui http post call....
229
- // should set the mime type where possible to intelligently do so.
230
-
231
-
232
-
233
-
234
- // may get some polymorphism out of the mime types.
235
- // text/plain;charset=UTF-8
236
- // turn it to a string.
237
-
238
-
239
-
240
254
  });
241
255
 
242
256
 
243
257
 
244
258
 
245
-
246
-
247
-
248
-
249
-
250
259
  /*
251
-
260
+
252
261
 
253
-
262
+
254
263
 
255
264
  */
256
265
 
@@ -102,7 +102,8 @@ class HTTP_Webpage_Publisher extends HTTP_Webpageorsite_Publisher {
102
102
  const render_webpage = async () => {
103
103
 
104
104
  const { webpage } = this;
105
- const Ctrl = webpage.content;
105
+ // Use ctrl (modern Webpage API) with fallback to content (legacy)
106
+ const Ctrl = webpage.ctrl || webpage.content;
106
107
 
107
108
  // In business activating it with the page context.
108
109
 
@@ -194,21 +195,98 @@ class HTTP_Webpage_Publisher extends HTTP_Webpageorsite_Publisher {
194
195
  }
195
196
 
196
197
 
197
- handle_http(req, res) {
198
- console.log('HTTP_Webpage_Publisher handle_http');
199
- console.log('req.url', req.url);
200
-
198
+ async handle_http(req, res) {
201
199
  const { webpage } = this;
200
+ // Use ctrl (modern Webpage API) with fallback to content (legacy)
201
+ const Ctrl = webpage.ctrl || webpage.content;
202
202
 
203
- const Ctrl = webpage.content;
204
- const ctrl = new Ctrl();
203
+ if (typeof Ctrl !== 'function') {
204
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
205
+ res.end('Admin page control is not available');
206
+ return;
207
+ }
205
208
 
206
- res.writeHead(200, {
207
- 'Content-Type': 'text/html'
208
- });
209
+ try {
210
+ const static_page_context = new Server_Static_Page_Context();
211
+
212
+ const ctrl = new Ctrl({
213
+ context: static_page_context
214
+ });
209
215
 
210
- res.end(ctrl.all_html_render());
216
+ let html;
211
217
 
218
+ if (ctrl.head && ctrl.body) {
219
+ // Control is a full document (has head/body) — inject CSS/JS directly
220
+ const ctrl_css_link = new jsgui_client.controls.link({
221
+ context: static_page_context
222
+ });
223
+ ctrl_css_link.dom.attributes.rel = 'stylesheet';
224
+ ctrl_css_link.dom.attributes.href = '/css/css.css';
225
+ ctrl.head.add(ctrl_css_link);
226
+
227
+ const ctrl_js_script_link = new jsgui_client.controls.script({
228
+ context: static_page_context
229
+ });
230
+ ctrl_js_script_link.dom.attributes.src = '/js/js.js';
231
+ ctrl.body.add(ctrl_js_script_link);
232
+
233
+ ctrl.active();
234
+ html = await ctrl.all_html_render();
235
+ } else {
236
+ // Control is not a document — wrap it in Active_HTML_Document
237
+ const Active_HTML_Document = require('../controls/Active_HTML_Document');
238
+
239
+ const doc = new Active_HTML_Document({
240
+ context: static_page_context
241
+ });
242
+
243
+ if (webpage.title) {
244
+ // Set the page title if available
245
+ const title = typeof webpage.get_title === 'function'
246
+ ? webpage.get_title() : webpage.title;
247
+ if (title && doc.head) {
248
+ const title_ctrl = new jsgui_client.controls.ctrl({
249
+ context: static_page_context,
250
+ 'dom.tagName': 'title'
251
+ });
252
+ title_ctrl.dom.text = title;
253
+ doc.head.add(title_ctrl);
254
+ }
255
+ }
256
+
257
+ // Add CSS link to head
258
+ const ctrl_css_link = new jsgui_client.controls.link({
259
+ context: static_page_context
260
+ });
261
+ ctrl_css_link.dom.attributes.rel = 'stylesheet';
262
+ ctrl_css_link.dom.attributes.href = '/css/css.css';
263
+ doc.head.add(ctrl_css_link);
264
+
265
+ // Add the control to body
266
+ doc.body.add(ctrl);
267
+
268
+ // Add JS script link to body
269
+ const ctrl_js_script_link = new jsgui_client.controls.script({
270
+ context: static_page_context
271
+ });
272
+ ctrl_js_script_link.dom.attributes.src = '/js/js.js';
273
+ doc.body.add(ctrl_js_script_link);
274
+
275
+ doc.active();
276
+ html = await doc.all_html_render();
277
+ }
278
+
279
+ res.writeHead(200, {
280
+ 'Content-Type': 'text/html; charset=utf-8'
281
+ });
282
+ res.end(html);
283
+ } catch (e) {
284
+ console.error('[HTTP_Webpage_Publisher] handle_http render error:', e);
285
+ if (!res.headersSent) {
286
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
287
+ res.end('Internal Server Error: Failed to render page');
288
+ }
289
+ }
212
290
  }
213
291
  }
214
292
 
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Query_Publisher — a Function_Publisher pre-configured for the
3
+ * standard data-view query protocol.
4
+ *
5
+ * Wraps a query function (typically from a Query_Resource) into an
6
+ * HTTP endpoint that:
7
+ * - accepts JSON params: { page, page_size, sort, filters }
8
+ * - returns JSON: { rows, total_count, page, page_size }
9
+ * - handles errors uniformly
10
+ *
11
+ * @example
12
+ * const Query_Publisher = require('jsgui3-server/publishers/query-publisher');
13
+ *
14
+ * // With a raw function:
15
+ * const pub = new Query_Publisher({
16
+ * name: 'users',
17
+ * query_fn: async (params) => {
18
+ * const rows = await db.all('SELECT ...');
19
+ * return { rows, total_count: 100 };
20
+ * }
21
+ * });
22
+ *
23
+ * // With a Query_Resource (preferred):
24
+ * const pub2 = Query_Publisher.from_resource(my_query_resource);
25
+ */
26
+
27
+ const Function_Publisher = require('./http-function-publisher');
28
+
29
+ class Query_Publisher extends Function_Publisher {
30
+ /**
31
+ * @param {Object} spec
32
+ * @param {string} spec.name - Endpoint name (used in route).
33
+ * @param {Function} spec.query_fn - async (params) => {rows, total_count}
34
+ * @param {Object} [spec.schema] - Column definitions for introspection.
35
+ * @param {number} [spec.default_page_size=25]
36
+ * @param {number} [spec.max_page_size=1000]
37
+ */
38
+ constructor(spec = {}) {
39
+ const query_fn = spec.query_fn;
40
+ if (typeof query_fn !== 'function') {
41
+ throw new Error('Query_Publisher requires a query_fn(params) function.');
42
+ }
43
+
44
+ const default_page_size = spec.default_page_size || 25;
45
+ const max_page_size = spec.max_page_size || 1000;
46
+
47
+ // Wrap the query_fn in a handler that normalizes input/output.
48
+ const handler_fn = async (input) => {
49
+ const params = input || {};
50
+
51
+ // Normalize params
52
+ const page = Math.max(1, Math.floor(Number(params.page) || 1));
53
+ const raw_page_size = Number(params.page_size);
54
+ const page_size = Number.isFinite(raw_page_size) && raw_page_size > 0
55
+ ? Math.min(Math.floor(raw_page_size), max_page_size)
56
+ : default_page_size;
57
+
58
+ let sort = null;
59
+ if (params.sort && typeof params.sort === 'object' && params.sort.key) {
60
+ sort = {
61
+ key: String(params.sort.key),
62
+ dir: String(params.sort.dir || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
63
+ };
64
+ }
65
+
66
+ let filters = null;
67
+ if (params.filters && typeof params.filters === 'object') {
68
+ filters = params.filters;
69
+ }
70
+
71
+ const normalized = { page, page_size, sort, filters };
72
+ const result = await query_fn(normalized);
73
+
74
+ return {
75
+ rows: Array.isArray(result.rows) ? result.rows : [],
76
+ total_count: Number.isFinite(result.total_count) ? result.total_count : 0,
77
+ page,
78
+ page_size
79
+ };
80
+ };
81
+
82
+ super({
83
+ fn: handler_fn,
84
+ name: spec.name,
85
+ schema: spec.schema || {}
86
+ });
87
+
88
+ this.type = 'query';
89
+ this.query_schema = spec.schema || {};
90
+ this.default_page_size = default_page_size;
91
+ this.max_page_size = max_page_size;
92
+ }
93
+
94
+ /**
95
+ * Create a Query_Publisher from a Query_Resource instance.
96
+ *
97
+ * @param {Query_Resource} resource - A Query_Resource with a query() method.
98
+ * @param {Object} [options] - Additional options (name override, schema override).
99
+ * @returns {Query_Publisher}
100
+ */
101
+ static from_resource(resource, options = {}) {
102
+ if (!resource || typeof resource.query !== 'function') {
103
+ throw new Error('from_resource requires a resource with a query() method.');
104
+ }
105
+
106
+ return new Query_Publisher({
107
+ name: options.name || resource.name,
108
+ query_fn: (params) => resource.query(params),
109
+ schema: options.schema || resource.schema,
110
+ default_page_size: options.default_page_size || resource.default_page_size,
111
+ max_page_size: options.max_page_size
112
+ });
113
+ }
114
+ }
115
+
116
+ module.exports = Query_Publisher;