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.
Files changed (77) 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/api-reference.md +120 -2
  5. package/docs/books/website-design/01-introduction.md +73 -0
  6. package/docs/books/website-design/02-current-state.md +195 -0
  7. package/docs/books/website-design/03-base-class.md +181 -0
  8. package/docs/books/website-design/04-webpage.md +307 -0
  9. package/docs/books/website-design/05-website.md +456 -0
  10. package/docs/books/website-design/06-pages-storage.md +170 -0
  11. package/docs/books/website-design/07-api-layer.md +285 -0
  12. package/docs/books/website-design/08-server-integration.md +271 -0
  13. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  14. package/docs/books/website-design/10-open-questions.md +196 -0
  15. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  16. package/docs/books/website-design/12-content-model.md +395 -0
  17. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  18. package/docs/books/website-design/14-website-module-spec.md +541 -0
  19. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  20. package/docs/books/website-design/16-minimal-first.md +203 -0
  21. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  22. package/docs/books/website-design/README.md +43 -0
  23. package/docs/configuration-reference.md +54 -0
  24. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  25. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  26. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  27. package/docs/swagger.md +316 -0
  28. package/examples/controls/1) window/server.js +6 -1
  29. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  30. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  31. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  32. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  33. declarative api/e2e-screenshot-1-name-change.png +0 -0
  34. declarative api/e2e-screenshot-2-toggled.png +0 -0
  35. declarative api/e2e-screenshot-3-final.png +0 -0
  36. declarative api/e2e-screenshot-final.png +0 -0
  37. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  38. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  39. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  40. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  41. package/examples/data-views/01) query-endpoint/server.js +61 -0
  42. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  43. package/labs/website-design/002-pages-storage/check.js +244 -0
  44. package/labs/website-design/002-pages-storage/results.txt +0 -0
  45. package/labs/website-design/003-type-detection/check.js +193 -0
  46. package/labs/website-design/003-type-detection/results.txt +0 -0
  47. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  48. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  49. package/labs/website-design/005-normalize-input/check.js +303 -0
  50. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  51. package/labs/website-design/README.md +34 -0
  52. package/labs/website-design/manifest.json +68 -0
  53. package/labs/website-design/run-all.js +60 -0
  54. package/middleware/json-body.js +126 -0
  55. package/openapi.js +474 -0
  56. package/package.json +11 -8
  57. package/publishers/Publishers.js +6 -5
  58. package/publishers/http-function-publisher.js +135 -126
  59. package/publishers/http-webpage-publisher.js +89 -11
  60. package/publishers/query-publisher.js +116 -0
  61. package/publishers/swagger-publisher.js +203 -0
  62. package/publishers/swagger-ui.js +578 -0
  63. package/resources/adapters/array-adapter.js +143 -0
  64. package/resources/query-resource.js +131 -0
  65. package/serve-factory.js +728 -18
  66. package/server.js +421 -103
  67. package/tests/README.md +23 -1
  68. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  69. package/tests/helpers/playwright-e2e-harness.js +326 -0
  70. package/tests/openapi.test.js +319 -0
  71. package/tests/playwright-smoke.test.js +134 -0
  72. package/tests/publish-enhancements.test.js +673 -0
  73. package/tests/query-publisher.test.js +430 -0
  74. package/tests/quick-json-body-test.js +169 -0
  75. package/tests/serve.test.js +425 -122
  76. package/tests/swagger-publisher.test.js +1076 -0
  77. 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;