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
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { describe, it, after, afterEach } = require('mocha');
|
|
4
|
+
|
|
5
|
+
const Array_Adapter = require('../resources/adapters/array-adapter');
|
|
6
|
+
const Query_Resource = require('../resources/query-resource');
|
|
7
|
+
const Query_Publisher = require('../publishers/query-publisher');
|
|
8
|
+
|
|
9
|
+
// ── Sample data ────────────────────────────────────────────────
|
|
10
|
+
const SAMPLE_DATA = [
|
|
11
|
+
{ id: 1, name: 'Alice', role: 'Engineer', score: 95 },
|
|
12
|
+
{ id: 2, name: 'Bob', role: 'Designer', score: 82 },
|
|
13
|
+
{ id: 3, name: 'Charlie', role: 'Engineer', score: 78 },
|
|
14
|
+
{ id: 4, name: 'Diana', role: 'Manager', score: 91 },
|
|
15
|
+
{ id: 5, name: 'Eve', role: 'Engineer', score: 88 },
|
|
16
|
+
{ id: 6, name: 'Frank', role: 'Designer', score: 74 },
|
|
17
|
+
{ id: 7, name: 'Grace', role: 'Manager', score: 97 },
|
|
18
|
+
{ id: 8, name: 'Hank', role: 'Engineer', score: 63 },
|
|
19
|
+
{ id: 9, name: 'Ivy', role: 'Designer', score: 85 },
|
|
20
|
+
{ id: 10, name: 'Jack', role: 'Manager', score: 90 }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// ── Array_Adapter tests ────────────────────────────────────────
|
|
24
|
+
describe('Array_Adapter', function () {
|
|
25
|
+
it('returns all rows with default params', async function () {
|
|
26
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
27
|
+
const result = await adapter.query();
|
|
28
|
+
|
|
29
|
+
assert.strictEqual(result.total_count, 10);
|
|
30
|
+
assert.strictEqual(result.rows.length, 10);
|
|
31
|
+
assert.strictEqual(result.page, 1);
|
|
32
|
+
assert.strictEqual(result.page_size, 25);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('paginates correctly', async function () {
|
|
36
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
37
|
+
const result = await adapter.query({ page: 2, page_size: 3 });
|
|
38
|
+
|
|
39
|
+
assert.strictEqual(result.total_count, 10);
|
|
40
|
+
assert.strictEqual(result.rows.length, 3);
|
|
41
|
+
assert.strictEqual(result.page, 2);
|
|
42
|
+
assert.strictEqual(result.page_size, 3);
|
|
43
|
+
assert.strictEqual(result.rows[0].name, 'Diana');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('handles page beyond data range', async function () {
|
|
47
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
48
|
+
const result = await adapter.query({ page: 100, page_size: 5 });
|
|
49
|
+
|
|
50
|
+
assert.strictEqual(result.total_count, 10);
|
|
51
|
+
assert.strictEqual(result.rows.length, 0);
|
|
52
|
+
assert.strictEqual(result.page, 100);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('sorts ascending by string', async function () {
|
|
56
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
57
|
+
const result = await adapter.query({ sort: { key: 'name', dir: 'asc' } });
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(result.rows[0].name, 'Alice');
|
|
60
|
+
assert.strictEqual(result.rows[result.rows.length - 1].name, 'Jack');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('sorts descending by number', async function () {
|
|
64
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
65
|
+
const result = await adapter.query({ sort: { key: 'score', dir: 'desc' } });
|
|
66
|
+
|
|
67
|
+
assert.strictEqual(result.rows[0].name, 'Grace');
|
|
68
|
+
assert.strictEqual(result.rows[0].score, 97);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('filters with contains op', async function () {
|
|
72
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
73
|
+
const result = await adapter.query({
|
|
74
|
+
filters: { name: { op: 'contains', value: 'a' } }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Alice, Charlie, Diana, Frank, Grace, Jack — names containing 'a'
|
|
78
|
+
for (const row of result.rows) {
|
|
79
|
+
assert(row.name.toLowerCase().includes('a'), `Expected ${row.name} to contain 'a'`);
|
|
80
|
+
}
|
|
81
|
+
assert.strictEqual(result.total_count, result.rows.length);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('filters with equals op', async function () {
|
|
85
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
86
|
+
const result = await adapter.query({
|
|
87
|
+
filters: { role: { op: 'equals', value: 'Engineer' } }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(result.total_count, 4);
|
|
91
|
+
for (const row of result.rows) {
|
|
92
|
+
assert.strictEqual(row.role, 'Engineer');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('filters with gt op', async function () {
|
|
97
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
98
|
+
const result = await adapter.query({
|
|
99
|
+
filters: { score: { op: 'gt', value: 90 } }
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (const row of result.rows) {
|
|
103
|
+
assert(row.score > 90, `Expected ${row.score} > 90`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('combines filter + sort + pagination', async function () {
|
|
108
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
109
|
+
const result = await adapter.query({
|
|
110
|
+
filters: { role: { op: 'equals', value: 'Engineer' } },
|
|
111
|
+
sort: { key: 'score', dir: 'desc' },
|
|
112
|
+
page: 1,
|
|
113
|
+
page_size: 2
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.strictEqual(result.total_count, 4); // 4 engineers
|
|
117
|
+
assert.strictEqual(result.rows.length, 2); // page_size 2
|
|
118
|
+
assert.strictEqual(result.rows[0].name, 'Alice'); // score 95
|
|
119
|
+
assert.strictEqual(result.rows[1].name, 'Eve'); // score 88
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles shorthand filter (string value implies contains)', async function () {
|
|
123
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
124
|
+
const result = await adapter.query({
|
|
125
|
+
filters: { name: 'bob' }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
assert.strictEqual(result.total_count, 1);
|
|
129
|
+
assert.strictEqual(result.rows[0].name, 'Bob');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('set_data replaces the data', async function () {
|
|
133
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
134
|
+
adapter.set_data([{ id: 99, name: 'Zara' }]);
|
|
135
|
+
const result = await adapter.query();
|
|
136
|
+
assert.strictEqual(result.total_count, 1);
|
|
137
|
+
assert.strictEqual(result.rows[0].name, 'Zara');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── Query_Resource tests ───────────────────────────────────────
|
|
142
|
+
describe('Query_Resource', function () {
|
|
143
|
+
it('requires an adapter with query method', function () {
|
|
144
|
+
assert.throws(() => {
|
|
145
|
+
new Query_Resource({ name: 'test' });
|
|
146
|
+
}, /adapter with a query/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('normalizes params and delegates to adapter', async function () {
|
|
150
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
151
|
+
const resource = new Query_Resource({
|
|
152
|
+
name: 'test',
|
|
153
|
+
adapter,
|
|
154
|
+
schema: { default_page_size: 5 }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = await resource.query({ page: '2' });
|
|
158
|
+
assert.strictEqual(result.page, 2);
|
|
159
|
+
assert.strictEqual(result.page_size, 5);
|
|
160
|
+
assert(Array.isArray(result.rows));
|
|
161
|
+
assert(Number.isFinite(result.total_count));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('clamps page_size to MAX_PAGE_SIZE', async function () {
|
|
165
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
166
|
+
const resource = new Query_Resource({ name: 'test', adapter });
|
|
167
|
+
|
|
168
|
+
const result = await resource.query({ page_size: 99999 });
|
|
169
|
+
assert.strictEqual(result.page_size, Query_Resource.MAX_PAGE_SIZE);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('defaults invalid page to 1', async function () {
|
|
173
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
174
|
+
const resource = new Query_Resource({ name: 'test', adapter });
|
|
175
|
+
|
|
176
|
+
const result = await resource.query({ page: -5 });
|
|
177
|
+
assert.strictEqual(result.page, 1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('start and stop delegate to adapter', async function () {
|
|
181
|
+
let started = false;
|
|
182
|
+
let stopped = false;
|
|
183
|
+
const adapter = {
|
|
184
|
+
start: () => { started = true; return Promise.resolve(); },
|
|
185
|
+
stop: () => { stopped = true; return Promise.resolve(); },
|
|
186
|
+
query: async () => ({ rows: [], total_count: 0 })
|
|
187
|
+
};
|
|
188
|
+
const resource = new Query_Resource({ name: 'test', adapter });
|
|
189
|
+
|
|
190
|
+
await new Promise((resolve) => resource.start(resolve));
|
|
191
|
+
assert.strictEqual(started, true);
|
|
192
|
+
|
|
193
|
+
await new Promise((resolve) => resource.stop(resolve));
|
|
194
|
+
assert.strictEqual(stopped, true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('start/stop work when adapter has no lifecycle methods', async function () {
|
|
198
|
+
const adapter = {
|
|
199
|
+
query: async () => ({ rows: [], total_count: 0 })
|
|
200
|
+
};
|
|
201
|
+
const resource = new Query_Resource({ name: 'test', adapter });
|
|
202
|
+
|
|
203
|
+
await new Promise((resolve) => resource.start(resolve));
|
|
204
|
+
await new Promise((resolve) => resource.stop(resolve));
|
|
205
|
+
// No error thrown
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── Query_Publisher tests ──────────────────────────────────────
|
|
210
|
+
describe('Query_Publisher', function () {
|
|
211
|
+
it('requires a query_fn', function () {
|
|
212
|
+
assert.throws(() => {
|
|
213
|
+
new Query_Publisher({ name: 'test' });
|
|
214
|
+
}, /query_fn/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('creates with a query_fn', function () {
|
|
218
|
+
const pub = new Query_Publisher({
|
|
219
|
+
name: 'test',
|
|
220
|
+
query_fn: async () => ({ rows: [], total_count: 0 })
|
|
221
|
+
});
|
|
222
|
+
assert.strictEqual(pub.type, 'query');
|
|
223
|
+
assert.strictEqual(pub.name, 'test');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('from_resource creates a publisher from a resource', function () {
|
|
227
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
228
|
+
const resource = new Query_Resource({ name: 'items', adapter });
|
|
229
|
+
const pub = Query_Publisher.from_resource(resource);
|
|
230
|
+
|
|
231
|
+
assert.strictEqual(pub.name, 'items');
|
|
232
|
+
assert.strictEqual(pub.type, 'query');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('from_resource requires a resource with query method', function () {
|
|
236
|
+
assert.throws(() => {
|
|
237
|
+
Query_Publisher.from_resource({});
|
|
238
|
+
}, /query.*method/);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── Integration: serve-factory data option ─────────────────────
|
|
243
|
+
describe('Server.serve with data option', function () {
|
|
244
|
+
this.timeout(20000);
|
|
245
|
+
|
|
246
|
+
// Stub out webpage/website publishers to avoid needing real controls.
|
|
247
|
+
const fake_webpage_publisher_path = require.resolve('../publishers/http-webpage-publisher');
|
|
248
|
+
const fake_website_publisher_path = require.resolve('../publishers/http-website-publisher');
|
|
249
|
+
const original_wp_module = require.cache[fake_webpage_publisher_path];
|
|
250
|
+
const original_ws_module = require.cache[fake_website_publisher_path];
|
|
251
|
+
|
|
252
|
+
const EventEmitter = require('events');
|
|
253
|
+
|
|
254
|
+
class Fake_Publisher extends EventEmitter {
|
|
255
|
+
constructor() {
|
|
256
|
+
super();
|
|
257
|
+
this.type = 'html';
|
|
258
|
+
this.extension = 'html';
|
|
259
|
+
setImmediate(() => this.emit('ready', { _arr: [] }));
|
|
260
|
+
}
|
|
261
|
+
handle_http(req, res) { res.writeHead(200); res.end('ok'); }
|
|
262
|
+
meets_requirements() { return true; }
|
|
263
|
+
start(cb) { if (cb) cb(null, true); return Promise.resolve(true); }
|
|
264
|
+
stop(cb) { if (cb) cb(null, true); return Promise.resolve(true); }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
require.cache[fake_webpage_publisher_path] = { exports: Fake_Publisher };
|
|
268
|
+
require.cache[fake_website_publisher_path] = { exports: Fake_Publisher };
|
|
269
|
+
|
|
270
|
+
const Server = require('../server');
|
|
271
|
+
const { get_free_port } = require('../port-utils');
|
|
272
|
+
|
|
273
|
+
const started_servers = [];
|
|
274
|
+
|
|
275
|
+
after(() => {
|
|
276
|
+
if (original_wp_module) {
|
|
277
|
+
require.cache[fake_webpage_publisher_path] = original_wp_module;
|
|
278
|
+
} else {
|
|
279
|
+
delete require.cache[fake_webpage_publisher_path];
|
|
280
|
+
}
|
|
281
|
+
if (original_ws_module) {
|
|
282
|
+
require.cache[fake_website_publisher_path] = original_ws_module;
|
|
283
|
+
} else {
|
|
284
|
+
delete require.cache[fake_website_publisher_path];
|
|
285
|
+
}
|
|
286
|
+
delete require.cache[require.resolve('../server')];
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
afterEach(async () => {
|
|
290
|
+
while (started_servers.length > 0) {
|
|
291
|
+
const s = started_servers.pop();
|
|
292
|
+
await new Promise((resolve) => s.close(() => resolve()));
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const http_post = (port, path, body) => {
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
const body_str = JSON.stringify(body);
|
|
299
|
+
const options = {
|
|
300
|
+
hostname: '127.0.0.1',
|
|
301
|
+
port,
|
|
302
|
+
path,
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: {
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
'Content-Length': Buffer.byteLength(body_str)
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const req = http.request(options, (res) => {
|
|
310
|
+
let data = '';
|
|
311
|
+
res.setEncoding('utf8');
|
|
312
|
+
res.on('data', (chunk) => data += chunk);
|
|
313
|
+
res.on('end', () => {
|
|
314
|
+
resolve({ statusCode: res.statusCode, body: data });
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
req.on('error', reject);
|
|
318
|
+
req.write(body_str);
|
|
319
|
+
req.end();
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
it('serves a data endpoint with array data', async function () {
|
|
324
|
+
const port = await get_free_port();
|
|
325
|
+
const server = await Server.serve({
|
|
326
|
+
host: '127.0.0.1',
|
|
327
|
+
port,
|
|
328
|
+
data: {
|
|
329
|
+
people: {
|
|
330
|
+
data: SAMPLE_DATA,
|
|
331
|
+
schema: {
|
|
332
|
+
columns: [
|
|
333
|
+
{ key: 'name', label: 'Name' },
|
|
334
|
+
{ key: 'role', label: 'Role' }
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
started_servers.push(server);
|
|
341
|
+
|
|
342
|
+
const { statusCode, body } = await http_post(port, '/api/data/people', {
|
|
343
|
+
page: 1,
|
|
344
|
+
page_size: 3
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
assert.strictEqual(statusCode, 200);
|
|
348
|
+
const parsed = JSON.parse(body);
|
|
349
|
+
assert.strictEqual(parsed.total_count, 10);
|
|
350
|
+
assert.strictEqual(parsed.rows.length, 3);
|
|
351
|
+
assert.strictEqual(parsed.page, 1);
|
|
352
|
+
assert.strictEqual(parsed.page_size, 3);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('serves a data endpoint with query_fn', async function () {
|
|
356
|
+
const port = await get_free_port();
|
|
357
|
+
const server = await Server.serve({
|
|
358
|
+
host: '127.0.0.1',
|
|
359
|
+
port,
|
|
360
|
+
data: {
|
|
361
|
+
custom: {
|
|
362
|
+
query_fn: async (params) => {
|
|
363
|
+
return {
|
|
364
|
+
rows: [{ value: 'hello' }],
|
|
365
|
+
total_count: 1
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
started_servers.push(server);
|
|
372
|
+
|
|
373
|
+
const { statusCode, body } = await http_post(port, '/api/data/custom', {});
|
|
374
|
+
|
|
375
|
+
assert.strictEqual(statusCode, 200);
|
|
376
|
+
const parsed = JSON.parse(body);
|
|
377
|
+
assert.strictEqual(parsed.total_count, 1);
|
|
378
|
+
assert.strictEqual(parsed.rows[0].value, 'hello');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('serves a data endpoint with adapter', async function () {
|
|
382
|
+
const port = await get_free_port();
|
|
383
|
+
const adapter = new Array_Adapter({ data: SAMPLE_DATA });
|
|
384
|
+
|
|
385
|
+
const server = await Server.serve({
|
|
386
|
+
host: '127.0.0.1',
|
|
387
|
+
port,
|
|
388
|
+
data: {
|
|
389
|
+
items: {
|
|
390
|
+
adapter,
|
|
391
|
+
schema: {}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
started_servers.push(server);
|
|
396
|
+
|
|
397
|
+
const { statusCode, body } = await http_post(port, '/api/data/items', {
|
|
398
|
+
sort: { key: 'score', dir: 'desc' },
|
|
399
|
+
page_size: 2
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
assert.strictEqual(statusCode, 200);
|
|
403
|
+
const parsed = JSON.parse(body);
|
|
404
|
+
assert.strictEqual(parsed.rows.length, 2);
|
|
405
|
+
assert.strictEqual(parsed.rows[0].name, 'Grace'); // highest score
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('filters data via the endpoint', async function () {
|
|
409
|
+
const port = await get_free_port();
|
|
410
|
+
const server = await Server.serve({
|
|
411
|
+
host: '127.0.0.1',
|
|
412
|
+
port,
|
|
413
|
+
data: {
|
|
414
|
+
team: { data: SAMPLE_DATA }
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
started_servers.push(server);
|
|
418
|
+
|
|
419
|
+
const { statusCode, body } = await http_post(port, '/api/data/team', {
|
|
420
|
+
filters: { role: { op: 'equals', value: 'Engineer' } }
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
assert.strictEqual(statusCode, 200);
|
|
424
|
+
const parsed = JSON.parse(body);
|
|
425
|
+
assert.strictEqual(parsed.total_count, 4);
|
|
426
|
+
for (const row of parsed.rows) {
|
|
427
|
+
assert.strictEqual(row.role, 'Engineer');
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const json_body = require('../middleware/json-body');
|
|
5
|
+
|
|
6
|
+
async function run() {
|
|
7
|
+
// Get free port
|
|
8
|
+
const port = await new Promise((resolve, reject) => {
|
|
9
|
+
const srv = net.createServer();
|
|
10
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
11
|
+
const p = srv.address().port;
|
|
12
|
+
srv.close(() => resolve(p));
|
|
13
|
+
});
|
|
14
|
+
srv.on('error', reject);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const mw = json_body({ limit: 1024 });
|
|
18
|
+
|
|
19
|
+
const server = http.createServer((req, res) => {
|
|
20
|
+
mw(req, res, () => {
|
|
21
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
22
|
+
res.end(JSON.stringify({
|
|
23
|
+
method: req.method,
|
|
24
|
+
body: req.body,
|
|
25
|
+
has_body: req.body !== undefined
|
|
26
|
+
}));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await new Promise(r => server.listen(port, '127.0.0.1', r));
|
|
31
|
+
console.log('Server on port', port);
|
|
32
|
+
|
|
33
|
+
const request = (method, path, body) => new Promise((resolve, reject) => {
|
|
34
|
+
const body_str = body ? JSON.stringify(body) : '';
|
|
35
|
+
const headers = body ? {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'Content-Length': Buffer.byteLength(body_str)
|
|
38
|
+
} : {};
|
|
39
|
+
const req = http.request({
|
|
40
|
+
hostname: '127.0.0.1', port, path, method, headers
|
|
41
|
+
}, (res) => {
|
|
42
|
+
let data = '';
|
|
43
|
+
res.on('data', c => data += c);
|
|
44
|
+
res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(data) }));
|
|
45
|
+
});
|
|
46
|
+
req.on('error', reject);
|
|
47
|
+
if (body_str) req.write(body_str);
|
|
48
|
+
req.end();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Test 1: POST with JSON body
|
|
52
|
+
console.log('Test 1: POST with JSON body');
|
|
53
|
+
const r1 = await request('POST', '/', { name: 'Alice', age: 30 });
|
|
54
|
+
assert.strictEqual(r1.status, 200);
|
|
55
|
+
assert.deepStrictEqual(r1.data.body, { name: 'Alice', age: 30 });
|
|
56
|
+
console.log(' PASS');
|
|
57
|
+
|
|
58
|
+
// Test 2: GET skips body parsing
|
|
59
|
+
console.log('Test 2: GET skips');
|
|
60
|
+
const r2 = await request('GET', '/');
|
|
61
|
+
assert.strictEqual(r2.status, 200);
|
|
62
|
+
assert.strictEqual(r2.data.has_body, false);
|
|
63
|
+
console.log(' PASS');
|
|
64
|
+
|
|
65
|
+
// Test 3: 413 Payload Too Large
|
|
66
|
+
console.log('Test 3: 413 Payload Too Large');
|
|
67
|
+
const big = { data: 'x'.repeat(2000) };
|
|
68
|
+
const r3 = await request('POST', '/', big);
|
|
69
|
+
assert.strictEqual(r3.status, 413);
|
|
70
|
+
console.log(' PASS');
|
|
71
|
+
|
|
72
|
+
// Test 4: Invalid JSON
|
|
73
|
+
console.log('Test 4: Invalid JSON → 400');
|
|
74
|
+
const r4 = await new Promise((resolve, reject) => {
|
|
75
|
+
const bad = '{not valid json';
|
|
76
|
+
const req = http.request({
|
|
77
|
+
hostname: '127.0.0.1', port, path: '/', method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bad) }
|
|
79
|
+
}, (res) => {
|
|
80
|
+
let data = '';
|
|
81
|
+
res.on('data', c => data += c);
|
|
82
|
+
res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(data) }));
|
|
83
|
+
});
|
|
84
|
+
req.on('error', reject);
|
|
85
|
+
req.write(bad);
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
assert.strictEqual(r4.status, 400);
|
|
89
|
+
assert.strictEqual(r4.data.error, 'Invalid JSON');
|
|
90
|
+
console.log(' PASS');
|
|
91
|
+
|
|
92
|
+
// Test 5: Empty body
|
|
93
|
+
console.log('Test 5: Empty POST body → null');
|
|
94
|
+
const r5 = await new Promise((resolve, reject) => {
|
|
95
|
+
const req = http.request({
|
|
96
|
+
hostname: '127.0.0.1', port, path: '/', method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': '0' }
|
|
98
|
+
}, (res) => {
|
|
99
|
+
let data = '';
|
|
100
|
+
res.on('data', c => data += c);
|
|
101
|
+
res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(data) }));
|
|
102
|
+
});
|
|
103
|
+
req.on('error', reject);
|
|
104
|
+
req.end();
|
|
105
|
+
});
|
|
106
|
+
assert.strictEqual(r5.status, 200);
|
|
107
|
+
assert.strictEqual(r5.data.body, null);
|
|
108
|
+
console.log(' PASS');
|
|
109
|
+
|
|
110
|
+
// Test 6: Non-JSON content-type skipped in strict mode
|
|
111
|
+
console.log('Test 6: Non-JSON content-type skipped');
|
|
112
|
+
const r6 = await new Promise((resolve, reject) => {
|
|
113
|
+
const body = 'hello=world';
|
|
114
|
+
const req = http.request({
|
|
115
|
+
hostname: '127.0.0.1', port, path: '/', method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body) }
|
|
117
|
+
}, (res) => {
|
|
118
|
+
let data = '';
|
|
119
|
+
res.on('data', c => data += c);
|
|
120
|
+
res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(data) }));
|
|
121
|
+
});
|
|
122
|
+
req.on('error', reject);
|
|
123
|
+
req.write(body);
|
|
124
|
+
req.end();
|
|
125
|
+
});
|
|
126
|
+
assert.strictEqual(r6.status, 200);
|
|
127
|
+
assert.strictEqual(r6.data.has_body, false);
|
|
128
|
+
console.log(' PASS');
|
|
129
|
+
|
|
130
|
+
// Test 7: Non-strict mode parses any content-type
|
|
131
|
+
console.log('Test 7: Non-strict mode');
|
|
132
|
+
const port2 = await new Promise((resolve, reject) => {
|
|
133
|
+
const srv = net.createServer();
|
|
134
|
+
srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => resolve(p)); });
|
|
135
|
+
});
|
|
136
|
+
const mw2 = json_body({ strict: false });
|
|
137
|
+
const server2 = http.createServer((req, res) => {
|
|
138
|
+
mw2(req, res, () => {
|
|
139
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
140
|
+
res.end(JSON.stringify({ body: req.body }));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
await new Promise(r => server2.listen(port2, '127.0.0.1', r));
|
|
144
|
+
const r7 = await new Promise((resolve, reject) => {
|
|
145
|
+
const body = '{"key":"val"}';
|
|
146
|
+
const req = http.request({
|
|
147
|
+
hostname: '127.0.0.1', port: port2, path: '/', method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(body) }
|
|
149
|
+
}, (res) => {
|
|
150
|
+
let data = '';
|
|
151
|
+
res.on('data', c => data += c);
|
|
152
|
+
res.on('end', () => resolve(JSON.parse(data)));
|
|
153
|
+
});
|
|
154
|
+
req.on('error', reject);
|
|
155
|
+
req.write(body);
|
|
156
|
+
req.end();
|
|
157
|
+
});
|
|
158
|
+
assert.deepStrictEqual(r7.body, { key: 'val' });
|
|
159
|
+
console.log(' PASS');
|
|
160
|
+
await new Promise(r => server2.close(r));
|
|
161
|
+
|
|
162
|
+
await new Promise(r => server.close(r));
|
|
163
|
+
console.log('\nAll 7 tests PASSED!');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
run().catch(err => {
|
|
167
|
+
console.error('FAILED:', err);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|