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.
- package/README.md +21 -0
- package/admin-ui/v1/controls/admin_shell.js +33 -0
- package/admin-ui/v1/server.js +14 -1
- package/docs/agi/skills/README.md +23 -0
- package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
- package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
- package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
- package/docs/agi/skills/deep-research/SKILL.md +156 -0
- package/docs/agi/skills/endurance/SKILL.md +53 -0
- package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
- package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
- package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
- package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
- package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
- package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
- package/docs/agi/skills/session-discipline/SKILL.md +40 -0
- package/docs/agi/skills/skill-writing/SKILL.md +211 -0
- package/docs/agi/skills/static-analysis/SKILL.md +58 -0
- package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
- package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
- package/docs/api-reference.md +120 -2
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/bundling-system-deep-dive.md +112 -3
- package/docs/configuration-reference.md +84 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/json-body.js +126 -0
- package/openapi.js +474 -0
- package/package.json +13 -7
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
- package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +677 -18
- package/server.js +585 -167
- package/tests/README.md +86 -2
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
- package/tests/control-elimination-root-feature-pruning.test.js +440 -0
- package/tests/control-elimination-static-bracket-access.test.js +245 -0
- package/tests/control-scan-manifest-regression.test.js +2 -0
- package/tests/end-to-end.test.js +22 -21
- package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/helpers/puppeteer-e2e-harness.js +62 -1
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- 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
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
} else {
|
|
117
|
-
obj_input = JSON.parse(inputStr);
|
|
160
|
+
if (inputStr.trim() !== '') {
|
|
161
|
+
body_input = JSON.parse(inputStr);
|
|
118
162
|
}
|
|
119
163
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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;
|