jsgui3-server 0.0.151 → 0.0.152
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/api-reference.md +120 -2
- 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/configuration-reference.md +54 -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 +11 -8
- 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/query-resource.js +131 -0
- package/serve-factory.js +728 -18
- package/server.js +421 -103
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -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 +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array_Adapter — in-memory data adapter for Query_Resource.
|
|
3
|
+
*
|
|
4
|
+
* Applies sort, filter, and pagination to a plain JS array.
|
|
5
|
+
* Useful for prototyping, small datasets, and tests.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const adapter = new Array_Adapter({ data: my_array });
|
|
9
|
+
* const result = await adapter.query({
|
|
10
|
+
* page: 1,
|
|
11
|
+
* page_size: 25,
|
|
12
|
+
* sort: { key: 'name', dir: 'asc' },
|
|
13
|
+
* filters: { name: { op: 'contains', value: 'alice' } }
|
|
14
|
+
* });
|
|
15
|
+
* // => { rows: [...], total_count: 142 }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const FILTER_OPS = {
|
|
19
|
+
contains(cell, value) {
|
|
20
|
+
return String(cell).toLowerCase().includes(String(value).toLowerCase());
|
|
21
|
+
},
|
|
22
|
+
equals(cell, value) {
|
|
23
|
+
// eslint-disable-next-line eqeqeq
|
|
24
|
+
return cell == value;
|
|
25
|
+
},
|
|
26
|
+
strict_equals(cell, value) {
|
|
27
|
+
return cell === value;
|
|
28
|
+
},
|
|
29
|
+
gt(cell, value) {
|
|
30
|
+
return Number(cell) > Number(value);
|
|
31
|
+
},
|
|
32
|
+
gte(cell, value) {
|
|
33
|
+
return Number(cell) >= Number(value);
|
|
34
|
+
},
|
|
35
|
+
lt(cell, value) {
|
|
36
|
+
return Number(cell) < Number(value);
|
|
37
|
+
},
|
|
38
|
+
lte(cell, value) {
|
|
39
|
+
return Number(cell) <= Number(value);
|
|
40
|
+
},
|
|
41
|
+
not_equals(cell, value) {
|
|
42
|
+
// eslint-disable-next-line eqeqeq
|
|
43
|
+
return cell != value;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
class Array_Adapter {
|
|
48
|
+
/**
|
|
49
|
+
* @param {Object} spec
|
|
50
|
+
* @param {Array} spec.data - The source array (kept by reference).
|
|
51
|
+
*/
|
|
52
|
+
constructor(spec = {}) {
|
|
53
|
+
this.data = spec.data || [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Replace the underlying data.
|
|
58
|
+
* @param {Array} data
|
|
59
|
+
*/
|
|
60
|
+
set_data(data) {
|
|
61
|
+
this.data = data || [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Query the data with sort, filter, and pagination.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} params
|
|
68
|
+
* @param {number} [params.page=1]
|
|
69
|
+
* @param {number} [params.page_size=25]
|
|
70
|
+
* @param {Object|null} [params.sort] - { key: string, dir: 'asc'|'desc' }
|
|
71
|
+
* @param {Object|null} [params.filters] - { column_key: { op: string, value: * }, ... }
|
|
72
|
+
* @returns {Promise<{rows: Array, total_count: number}>}
|
|
73
|
+
*/
|
|
74
|
+
async query(params = {}) {
|
|
75
|
+
let rows = this.data.slice();
|
|
76
|
+
|
|
77
|
+
// ── Filter ──────────────────────────────────────────────
|
|
78
|
+
if (params.filters && typeof params.filters === 'object') {
|
|
79
|
+
const filter_entries = Object.entries(params.filters);
|
|
80
|
+
for (const [key, filter_spec] of filter_entries) {
|
|
81
|
+
if (!filter_spec) continue;
|
|
82
|
+
|
|
83
|
+
let op_name, filter_value;
|
|
84
|
+
if (typeof filter_spec === 'object' && filter_spec.op) {
|
|
85
|
+
op_name = filter_spec.op;
|
|
86
|
+
filter_value = filter_spec.value;
|
|
87
|
+
} else {
|
|
88
|
+
// Shorthand: { name: 'alice' } implies contains
|
|
89
|
+
op_name = 'contains';
|
|
90
|
+
filter_value = filter_spec;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const op_fn = FILTER_OPS[op_name];
|
|
94
|
+
if (!op_fn) continue;
|
|
95
|
+
|
|
96
|
+
rows = rows.filter(row => {
|
|
97
|
+
const cell = row[key];
|
|
98
|
+
return op_fn(cell, filter_value);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const total_count = rows.length;
|
|
104
|
+
|
|
105
|
+
// ── Sort ────────────────────────────────────────────────
|
|
106
|
+
if (params.sort && params.sort.key) {
|
|
107
|
+
const sort_key = params.sort.key;
|
|
108
|
+
const dir = String(params.sort.dir || 'asc').toLowerCase() === 'desc' ? -1 : 1;
|
|
109
|
+
|
|
110
|
+
rows.sort((a, b) => {
|
|
111
|
+
const av = a[sort_key];
|
|
112
|
+
const bv = b[sort_key];
|
|
113
|
+
|
|
114
|
+
if (av === bv) return 0;
|
|
115
|
+
if (av === null || av === undefined) return 1;
|
|
116
|
+
if (bv === null || bv === undefined) return -1;
|
|
117
|
+
|
|
118
|
+
if (typeof av === 'number' && typeof bv === 'number') {
|
|
119
|
+
return (av - bv) * dir;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return String(av).localeCompare(String(bv)) * dir;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Paginate ────────────────────────────────────────────
|
|
127
|
+
const page_size = Math.max(1, Number(params.page_size) || 25);
|
|
128
|
+
const page = Math.max(1, Number(params.page) || 1);
|
|
129
|
+
const start = (page - 1) * page_size;
|
|
130
|
+
const paged_rows = rows.slice(start, start + page_size);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
rows: paged_rows,
|
|
134
|
+
total_count,
|
|
135
|
+
page,
|
|
136
|
+
page_size
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Array_Adapter.FILTER_OPS = FILTER_OPS;
|
|
142
|
+
|
|
143
|
+
module.exports = Array_Adapter;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query_Resource — a Resource subclass that holds a data adapter
|
|
3
|
+
* and exposes a standard query(params) method.
|
|
4
|
+
*
|
|
5
|
+
* Pluggable adapters let you swap the backing store (in-memory array,
|
|
6
|
+
* SQL database, REST API, etc.) while keeping a uniform interface.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const Query_Resource = require('jsgui3-server/resources/query-resource');
|
|
10
|
+
* const Array_Adapter = require('jsgui3-server/resources/adapters/array-adapter');
|
|
11
|
+
*
|
|
12
|
+
* const resource = new Query_Resource({
|
|
13
|
+
* name: 'products',
|
|
14
|
+
* adapter: new Array_Adapter({ data: products }),
|
|
15
|
+
* schema: {
|
|
16
|
+
* columns: [
|
|
17
|
+
* { key: 'name', label: 'Name', sortable: true, filterable: true },
|
|
18
|
+
* { key: 'price', label: 'Price', sortable: true }
|
|
19
|
+
* ],
|
|
20
|
+
* default_page_size: 25
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { Resource } = require('jsgui3-html');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
28
|
+
const MAX_PAGE_SIZE = 1000;
|
|
29
|
+
|
|
30
|
+
class Query_Resource extends Resource {
|
|
31
|
+
/**
|
|
32
|
+
* @param {Object} spec
|
|
33
|
+
* @param {string} spec.name - Resource name (visible in admin-ui / resource pool).
|
|
34
|
+
* @param {Object} spec.adapter - Data adapter with a `query(params)` method.
|
|
35
|
+
* @param {Object} [spec.schema] - Column definitions, default_page_size, etc.
|
|
36
|
+
*/
|
|
37
|
+
constructor(spec = {}) {
|
|
38
|
+
super(spec);
|
|
39
|
+
if (!spec.adapter || typeof spec.adapter.query !== 'function') {
|
|
40
|
+
throw new Error('Query_Resource requires an adapter with a query(params) method.');
|
|
41
|
+
}
|
|
42
|
+
this.adapter = spec.adapter;
|
|
43
|
+
this.schema = spec.schema || {};
|
|
44
|
+
this.default_page_size = (this.schema && this.schema.default_page_size) || DEFAULT_PAGE_SIZE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start the resource (and the adapter if it supports it).
|
|
49
|
+
*/
|
|
50
|
+
start(callback) {
|
|
51
|
+
if (this.adapter && typeof this.adapter.start === 'function') {
|
|
52
|
+
const result = this.adapter.start();
|
|
53
|
+
if (result && typeof result.then === 'function') {
|
|
54
|
+
return result.then(
|
|
55
|
+
() => { if (typeof callback === 'function') callback(null, true); },
|
|
56
|
+
(err) => { if (typeof callback === 'function') callback(err); else throw err; }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (typeof callback === 'function') callback(null, true);
|
|
61
|
+
return Promise.resolve(true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stop the resource (and the adapter if it supports it).
|
|
66
|
+
*/
|
|
67
|
+
stop(callback) {
|
|
68
|
+
if (this.adapter && typeof this.adapter.stop === 'function') {
|
|
69
|
+
const result = this.adapter.stop();
|
|
70
|
+
if (result && typeof result.then === 'function') {
|
|
71
|
+
return result.then(
|
|
72
|
+
() => { if (typeof callback === 'function') callback(null, true); },
|
|
73
|
+
(err) => { if (typeof callback === 'function') callback(err); else throw err; }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (typeof callback === 'function') callback(null, true);
|
|
78
|
+
return Promise.resolve(true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Normalize and validate query params, then delegate to the adapter.
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} params - Raw query params from the client.
|
|
85
|
+
* @returns {Promise<{rows: Array, total_count: number, page: number, page_size: number}>}
|
|
86
|
+
*/
|
|
87
|
+
async query(params = {}) {
|
|
88
|
+
const normalized = this._normalize_params(params);
|
|
89
|
+
const result = await this.adapter.query(normalized);
|
|
90
|
+
|
|
91
|
+
// Ensure the response has the standard shape.
|
|
92
|
+
return {
|
|
93
|
+
rows: Array.isArray(result.rows) ? result.rows : [],
|
|
94
|
+
total_count: Number.isFinite(result.total_count) ? result.total_count : 0,
|
|
95
|
+
page: normalized.page,
|
|
96
|
+
page_size: normalized.page_size
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Normalize raw params into a clean query spec.
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
_normalize_params(params = {}) {
|
|
105
|
+
const page = Math.max(1, Math.floor(Number(params.page) || 1));
|
|
106
|
+
const raw_page_size = Number(params.page_size);
|
|
107
|
+
const page_size = Number.isFinite(raw_page_size) && raw_page_size > 0
|
|
108
|
+
? Math.min(Math.floor(raw_page_size), MAX_PAGE_SIZE)
|
|
109
|
+
: this.default_page_size;
|
|
110
|
+
|
|
111
|
+
let sort = null;
|
|
112
|
+
if (params.sort && typeof params.sort === 'object' && params.sort.key) {
|
|
113
|
+
sort = {
|
|
114
|
+
key: String(params.sort.key),
|
|
115
|
+
dir: String(params.sort.dir || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let filters = null;
|
|
120
|
+
if (params.filters && typeof params.filters === 'object') {
|
|
121
|
+
filters = params.filters;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { page, page_size, sort, filters };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Query_Resource.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
129
|
+
Query_Resource.MAX_PAGE_SIZE = MAX_PAGE_SIZE;
|
|
130
|
+
|
|
131
|
+
module.exports = Query_Resource;
|