@loadbare/app 0.8.2 → 0.10.0
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 +3 -3
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +74 -66
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/expand.d.ts.map +1 -1
- package/dist/build/expand.js +20 -19
- package/dist/build/expand.js.map +1 -1
- package/dist/build/locations.d.ts +2 -3
- package/dist/build/locations.d.ts.map +1 -1
- package/dist/build/locations.js +2 -3
- package/dist/build/locations.js.map +1 -1
- package/dist/build/pages.d.ts +3 -4
- package/dist/build/pages.d.ts.map +1 -1
- package/dist/build/pages.js +3 -4
- package/dist/build/pages.js.map +1 -1
- package/dist/core/lb-constants.d.ts +25 -23
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +95 -158
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +73 -75
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +58 -5
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +47 -37
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +174 -193
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +419 -415
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts +20 -13
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +50 -52
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +81 -116
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +151 -48
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +893 -558
- package/docs/comparison.md +222 -185
- package/docs/prior-art.md +15 -14
- package/docs/reference/builder.md +9 -3
- package/docs/reference/chrome.md +107 -56
- package/docs/reference/custom-elements.md +199 -173
- package/docs/reference/data-binding.md +375 -370
- package/docs/reference/overview.md +12 -10
- package/docs/reference/page-files.md +161 -86
- package/docs/reference/server.md +13 -7
- package/docs/reference/widgets.md +104 -110
- package/docs/roadmap.md +36 -31
- package/docs/terms-of-art.md +57 -0
- package/docs/testing.md +97 -68
- package/docs/theory.md +92 -58
- package/docs/tutorials/010-pages-and-navigation.md +20 -12
- package/docs/tutorials/020-css.md +6 -3
- package/docs/tutorials/030-html-decomposition.md +9 -7
- package/docs/tutorials/040-displaying-data.md +30 -13
- package/docs/tutorials/{050-actions.md → 050-requests.md} +25 -15
- package/docs/tutorials/060-custom-element-code.md +17 -16
- package/docs/tutorials/065-conditional-rendering.md +34 -23
- package/docs/tutorials/070-displaying-a-list.md +29 -21
- package/docs/tutorials/072-inserting-into-a-list.md +24 -16
- package/docs/tutorials/074-deleting-from-a-list.md +9 -7
- package/docs/tutorials/076-updating-a-list-item.md +11 -10
- package/docs/tutorials/080-widget-requests.md +71 -43
- package/docs/tutorials/090-using-widget-libraries.md +22 -22
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +178 -111
- package/skills/loadbare-app/references/TECHREF-1.0.md +893 -558
- package/skills/loadbare-app/references/builder.md +9 -3
- package/skills/loadbare-app/references/chrome.md +107 -56
- package/skills/loadbare-app/references/custom-elements.md +199 -173
- package/skills/loadbare-app/references/data-binding.md +375 -370
- package/skills/loadbare-app/references/overview.md +12 -10
- package/skills/loadbare-app/references/page-files.md +161 -86
- package/skills/loadbare-app/references/server.md +13 -7
- package/skills/loadbare-app/references/widgets.md +104 -110
- package/docs/analysis-accidental-complexity.md +0 -149
- package/docs/analysis-closed-set.md +0 -210
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HubData, HubRequest, HubResponse, HubResult, Kind, Patch, RequestFields, Row } from "../core/lb-types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Whatever the application hands the engine for the duration of one request.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* declaration merging, once, anywhere in its own source:
|
|
5
|
+
* Loadbare declares it empty and never reads it. An application fills it in
|
|
6
|
+
* by declaration merging, once, anywhere in its own source:
|
|
7
7
|
*
|
|
8
8
|
* declare module "@loadbare/app/server" {
|
|
9
9
|
* interface HubContext {
|
|
@@ -21,169 +21,134 @@ import type { Patch, Row, HubData, HubResult } from "../core/lb-types.js";
|
|
|
21
21
|
export interface HubContext {
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* A declared query:
|
|
24
|
+
* A declared query: its kind, its key, and the run that answers it.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* with
|
|
29
|
-
*
|
|
26
|
+
* Kind and key are properties of the name rather than of any one answer, so
|
|
27
|
+
* they are declared here and never inferred from what comes back. The engine
|
|
28
|
+
* sends both with every answer, so the markup repeats neither. Every query
|
|
29
|
+
* has a key; an aggregate row answers with a constant one.
|
|
30
30
|
*
|
|
31
|
-
* Write one with `row()` or `
|
|
31
|
+
* Write one with `row()` or `rows()` below; nothing else builds one.
|
|
32
32
|
*/
|
|
33
33
|
export interface Query {
|
|
34
|
-
readonly kind:
|
|
34
|
+
readonly kind: Kind;
|
|
35
|
+
readonly key: string;
|
|
35
36
|
readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;
|
|
36
37
|
}
|
|
37
|
-
/** A query that answers with one row. */
|
|
38
|
-
export declare function row(run: (ctx: HubContext) => Row | Promise<Row>): Query;
|
|
38
|
+
/** A query that answers with one row, identified by its `key` column. */
|
|
39
|
+
export declare function row(key: string, run: (ctx: HubContext) => Row | Promise<Row>): Query;
|
|
39
40
|
/**
|
|
40
|
-
* A query that answers with
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* There is no wrapper around the array, because the declaration already said
|
|
44
|
-
* this name answers with rows.
|
|
41
|
+
* A query that answers with all its rows, and therefore also their order.
|
|
42
|
+
* Rows land by their `key` column: a row whose key is not in the answer is
|
|
43
|
+
* gone.
|
|
45
44
|
*/
|
|
46
|
-
export declare function
|
|
45
|
+
export declare function rows(key: string, run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query;
|
|
47
46
|
/**
|
|
48
|
-
* Only what changed, returned from a
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
47
|
+
* Only what changed, returned from a handler for a `rows` query. Rows named
|
|
48
|
+
* here are added or updated, keys in `drop` are removed, and everything
|
|
49
|
+
* unnamed is left alone — its contents, and its place in whatever order the
|
|
50
|
+
* page is keeping.
|
|
52
51
|
*
|
|
53
|
-
* `Array.isArray` is what tells a patch from
|
|
52
|
+
* `Array.isArray` is what tells a patch from all rows, so a patch needs no
|
|
54
53
|
* marker of its own and no column name is reserved to carry one.
|
|
55
54
|
*/
|
|
56
55
|
export declare function patch(change: {
|
|
57
56
|
rows?: Row[];
|
|
58
|
-
drop?:
|
|
57
|
+
drop?: unknown[];
|
|
59
58
|
}): Patch;
|
|
60
|
-
/** The shape of a `<name>.queries.ts` module. */
|
|
61
|
-
export type Queries = Record<string, Query>;
|
|
62
59
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* A new row for `lb-url`, returned from a handler when only the handler can
|
|
61
|
+
* know where the page belongs: the key of a row it inserted, or the absence
|
|
62
|
+
* of one it deleted.
|
|
65
63
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* and there is nowhere else to put it; it is a string from a control, not an
|
|
70
|
-
* argument list.
|
|
71
|
-
*/
|
|
72
|
-
export interface Where {
|
|
73
|
-
list?: string;
|
|
74
|
-
row?: string;
|
|
75
|
-
key?: string;
|
|
76
|
-
cell?: string;
|
|
77
|
-
value?: string;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* What an action does, and which queries must re-run once it has.
|
|
64
|
+
* A column other than `lb-path` is a query parm: set, or taken out when its
|
|
65
|
+
* value is empty. With `lb-path` naming another page, the page is entered
|
|
66
|
+
* with only the parms named here; otherwise every other parm is kept.
|
|
81
67
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
68
|
+
* The page then loads at the new URL in the same round trip, as a cold load
|
|
69
|
+
* of it would, so the refresh set is not run and whatever else the handler
|
|
70
|
+
* returned is dropped: both were answers for the URL the page is leaving.
|
|
71
|
+
* This is Post/Redirect/Get without the redirect.
|
|
86
72
|
*/
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
73
|
+
export declare function url(columns: Record<string, string>): HubData;
|
|
74
|
+
/** The shape of a `<stub>.queries.ts` module. */
|
|
75
|
+
export type Queries = Record<string, Query>;
|
|
91
76
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
77
|
+
* What a handler does, and which queries re-run once it has.
|
|
78
|
+
*
|
|
79
|
+
* `run` may also return answers of its own, which are laid over the
|
|
80
|
+
* refreshed ones. That is how a patch reaches the browser: a query answers
|
|
81
|
+
* for all its rows and cannot know why it re-ran, but the handler knows
|
|
82
|
+
* exactly what it changed and can say only that.
|
|
95
83
|
*/
|
|
96
|
-
export interface
|
|
97
|
-
run: (ctx: HubContext,
|
|
84
|
+
export interface Handler<R = RequestFields> {
|
|
85
|
+
run: (ctx: HubContext, request: R) => void | HubData | Promise<void | HubData>;
|
|
98
86
|
refresh: string[];
|
|
99
87
|
}
|
|
100
|
-
export type
|
|
101
|
-
key
|
|
102
|
-
}>;
|
|
103
|
-
export type RowInsertOp = CrudOp<{
|
|
88
|
+
export type RowInsertHandler = Handler<{
|
|
89
|
+
key?: string;
|
|
104
90
|
values: Record<string, string>;
|
|
105
91
|
}>;
|
|
106
|
-
/** `values` holds only the columns being set
|
|
107
|
-
export type
|
|
92
|
+
/** `values` holds only the columns being set, as an SQL UPDATE sets them. */
|
|
93
|
+
export type RowUpdateHandler = Handler<{
|
|
108
94
|
key: string;
|
|
109
95
|
values: Record<string, string>;
|
|
110
96
|
}>;
|
|
97
|
+
export type RowDeleteHandler = Handler<{
|
|
98
|
+
key: string;
|
|
99
|
+
}>;
|
|
111
100
|
/**
|
|
112
|
-
*
|
|
113
|
-
* `Requests.crud`. A
|
|
114
|
-
* cannot reach anything the page has not published
|
|
115
|
-
* action.
|
|
101
|
+
* What the requests Loadbare provides do to one query, keyed by its name in
|
|
102
|
+
* `Requests.crud`. A query with no entry here permits none of them: the wire
|
|
103
|
+
* cannot reach anything the page has not published.
|
|
116
104
|
*
|
|
117
|
-
* Every key
|
|
118
|
-
*
|
|
119
|
-
* are one vocabulary. All three are list operations: each needs a key, and a
|
|
120
|
-
* key exists only on a live row inside a list.
|
|
105
|
+
* Every key is the request name with the prefix stripped and the rest
|
|
106
|
+
* camel-cased: `lb-row-insert` runs `rowInsert`.
|
|
121
107
|
*/
|
|
122
108
|
export interface Crud {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
109
|
+
rowInsert?: RowInsertHandler;
|
|
110
|
+
rowUpdate?: RowUpdateHandler;
|
|
111
|
+
rowDelete?: RowDeleteHandler;
|
|
126
112
|
}
|
|
127
113
|
/**
|
|
128
|
-
* The shape of a `<
|
|
114
|
+
* The shape of a `<stub>.requests.ts` module.
|
|
129
115
|
*
|
|
130
|
-
* `onPageEnter` runs
|
|
131
|
-
*
|
|
132
|
-
*
|
|
116
|
+
* `onPageEnter` runs when the page loads, before its queries. It declares no
|
|
117
|
+
* refresh set: the page's queries all run afterward, so whatever it changed
|
|
118
|
+
* is already in the response.
|
|
133
119
|
*
|
|
134
|
-
* `
|
|
135
|
-
* declare is refused, so the wire cannot reach anything the page has
|
|
136
|
-
* published.
|
|
120
|
+
* `handlers` holds the page's declared requests, by name. A name the page
|
|
121
|
+
* did not declare is refused, so the wire cannot reach anything the page has
|
|
122
|
+
* not published.
|
|
137
123
|
*
|
|
138
|
-
* `crud`
|
|
139
|
-
* they operate on rather than by a declared name — there is nothing to name,
|
|
140
|
-
* since the row's own binding says what it is.
|
|
124
|
+
* `crud` holds what the requests Loadbare provides run, by query name.
|
|
141
125
|
*/
|
|
142
126
|
export interface Requests {
|
|
143
127
|
onPageEnter?: (ctx: HubContext) => void | Promise<void>;
|
|
144
|
-
|
|
128
|
+
handlers?: Record<string, Handler>;
|
|
145
129
|
crud?: Record<string, Crud>;
|
|
146
130
|
}
|
|
147
|
-
/** A page is three files sharing a
|
|
131
|
+
/** A page is three files sharing a stub; two of them are these. */
|
|
148
132
|
export interface Page {
|
|
149
133
|
queries: Queries;
|
|
150
134
|
requests: Requests;
|
|
151
135
|
}
|
|
152
136
|
/**
|
|
153
|
-
* The page registry. The builder
|
|
154
|
-
* `.
|
|
155
|
-
* "Generating the server-side page registry".
|
|
137
|
+
* The page registry. The builder writes it from every `.requests.ts` and
|
|
138
|
+
* `.queries.ts` it discovers — see docs/reference/builder.md.
|
|
156
139
|
*/
|
|
157
140
|
export type Pages = Record<string, Page>;
|
|
141
|
+
/**
|
|
142
|
+
* The engine. Both calls answer with response items. A request whose
|
|
143
|
+
* handler returned `url()` answers with the `lb-url` item alone: the caller
|
|
144
|
+
* builds a context at the new URL, calls `dataForPage` with it, and sends
|
|
145
|
+
* the load beside the item. `hubRoutes` does exactly that.
|
|
146
|
+
*/
|
|
158
147
|
export interface Hub {
|
|
159
|
-
/**
|
|
160
|
-
dataForPage(page: string, ctx: HubContext): Promise<
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
* set declared with it.
|
|
164
|
-
*/
|
|
165
|
-
runAction(page: string, name: string, where: Where, ctx: HubContext): Promise<HubData>;
|
|
166
|
-
/** Drop one row: run the list's declared `rowDelete`, then its refresh set. */
|
|
167
|
-
runRowDelete(page: string, where: {
|
|
168
|
-
list: string;
|
|
169
|
-
key: string;
|
|
170
|
-
}, ctx: HubContext): Promise<HubData>;
|
|
171
|
-
/** Add one row: run the list's declared `rowInsert`, then its refresh set. */
|
|
172
|
-
runRowInsert(page: string, where: {
|
|
173
|
-
list: string;
|
|
174
|
-
values: Record<string, string>;
|
|
175
|
-
}, ctx: HubContext): Promise<HubData>;
|
|
176
|
-
/**
|
|
177
|
-
* Edit a row: run the list's declared `rowUpdate`, then its refresh set.
|
|
178
|
-
* `values` holds only the columns being set — one from a widget that
|
|
179
|
-
* commits a cell, several from a form — and a column absent from it is left
|
|
180
|
-
* as it is, as an SQL UPDATE leaves it.
|
|
181
|
-
*/
|
|
182
|
-
runRowUpdate(page: string, where: {
|
|
183
|
-
list: string;
|
|
184
|
-
key: string;
|
|
185
|
-
values: Record<string, string>;
|
|
186
|
-
}, ctx: HubContext): Promise<HubData>;
|
|
148
|
+
/** A page load: its `onPageEnter`, then all of its queries. */
|
|
149
|
+
dataForPage(page: string, ctx: HubContext): Promise<HubResponse>;
|
|
150
|
+
/** A request: run the handler its name picks, then the refresh set. */
|
|
151
|
+
runRequest(page: string, request: HubRequest, ctx: HubContext): Promise<HubResponse>;
|
|
187
152
|
}
|
|
188
153
|
/**
|
|
189
154
|
* Build the engine over a set of pages. The pages are fixed at startup; the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
|
package/dist/server/lb-server.js
CHANGED
|
@@ -2,48 +2,78 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This is the counterpart to core/lb-constants.ts: that file is the
|
|
4
4
|
// vocabulary the browser writes, this one is the vocabulary the application
|
|
5
|
-
// writes. The engine below is the whole of
|
|
6
|
-
// HTTP server, no router, and no data layer.
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
// writes. The engine below is the whole of Loadbare on the server. It ships
|
|
6
|
+
// no HTTP server, no router, and no data layer.
|
|
7
|
+
import { isRowRequest } from "../core/lb-types.js";
|
|
8
|
+
import { KIND_ROW, KIND_ROWS, LB_RESERVED_PREFIX, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
|
|
9
|
+
/** A query that answers with one row, identified by its `key` column. */
|
|
10
|
+
export function row(key, run) {
|
|
11
|
+
return { kind: KIND_ROW, key, run };
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
|
-
* A query that answers with
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* There is no wrapper around the array, because the declaration already said
|
|
17
|
-
* this name answers with rows.
|
|
14
|
+
* A query that answers with all its rows, and therefore also their order.
|
|
15
|
+
* Rows land by their `key` column: a row whose key is not in the answer is
|
|
16
|
+
* gone.
|
|
18
17
|
*/
|
|
19
|
-
export function
|
|
20
|
-
return { kind:
|
|
18
|
+
export function rows(key, run) {
|
|
19
|
+
return { kind: KIND_ROWS, key, run };
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
|
-
* Only what changed, returned from a
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* Only what changed, returned from a handler for a `rows` query. Rows named
|
|
23
|
+
* here are added or updated, keys in `drop` are removed, and everything
|
|
24
|
+
* unnamed is left alone — its contents, and its place in whatever order the
|
|
25
|
+
* page is keeping.
|
|
27
26
|
*
|
|
28
|
-
* `Array.isArray` is what tells a patch from
|
|
27
|
+
* `Array.isArray` is what tells a patch from all rows, so a patch needs no
|
|
29
28
|
* marker of its own and no column name is reserved to carry one.
|
|
30
29
|
*/
|
|
31
30
|
export function patch(change) {
|
|
32
|
-
|
|
31
|
+
const made = { ...change };
|
|
32
|
+
patches.add(made);
|
|
33
|
+
return made;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every object `patch()` made. A patch and a row are both plain objects, so
|
|
37
|
+
* this is how the engine refuses a patch answering a `row` query.
|
|
38
|
+
*/
|
|
39
|
+
const patches = new WeakSet();
|
|
40
|
+
/**
|
|
41
|
+
* A new row for `lb-url`, returned from a handler when only the handler can
|
|
42
|
+
* know where the page belongs: the key of a row it inserted, or the absence
|
|
43
|
+
* of one it deleted.
|
|
44
|
+
*
|
|
45
|
+
* A column other than `lb-path` is a query parm: set, or taken out when its
|
|
46
|
+
* value is empty. With `lb-path` naming another page, the page is entered
|
|
47
|
+
* with only the parms named here; otherwise every other parm is kept.
|
|
48
|
+
*
|
|
49
|
+
* The page then loads at the new URL in the same round trip, as a cold load
|
|
50
|
+
* of it would, so the refresh set is not run and whatever else the handler
|
|
51
|
+
* returned is dropped: both were answers for the URL the page is leaving.
|
|
52
|
+
* This is Post/Redirect/Get without the redirect.
|
|
53
|
+
*/
|
|
54
|
+
export function url(columns) {
|
|
55
|
+
return { [URL_QUERY]: { ...columns } };
|
|
33
56
|
}
|
|
57
|
+
/** The camel-cased `Crud` key of a request Loadbare provides. */
|
|
58
|
+
const CRUD_KEYS = {
|
|
59
|
+
[REQUEST_ROW_INSERT]: "rowInsert",
|
|
60
|
+
[REQUEST_ROW_UPDATE]: "rowUpdate",
|
|
61
|
+
[REQUEST_ROW_DELETE]: "rowDelete",
|
|
62
|
+
};
|
|
34
63
|
/**
|
|
35
64
|
* Build the engine over a set of pages. The pages are fixed at startup; the
|
|
36
65
|
* context is not, and arrives with each call.
|
|
37
66
|
*/
|
|
38
67
|
export function createHub(pages) {
|
|
39
|
-
// Names beginning with the reserved prefix are Loadbare's —
|
|
40
|
-
//
|
|
68
|
+
// Names beginning with the reserved prefix are Loadbare's — its own query,
|
|
69
|
+
// the requests it provides — so an application cannot declare one.
|
|
41
70
|
// Refused at startup, because a name is a fact about the page and not
|
|
42
71
|
// about any request.
|
|
43
72
|
for (const [page, entry] of Object.entries(pages)) {
|
|
44
73
|
const declared = [
|
|
45
74
|
...Object.keys(entry.queries),
|
|
46
|
-
...Object.keys(entry.requests.
|
|
75
|
+
...Object.keys(entry.requests.handlers ?? {}),
|
|
76
|
+
...Object.keys(entry.requests.crud ?? {}),
|
|
47
77
|
];
|
|
48
78
|
for (const name of declared) {
|
|
49
79
|
if (name.startsWith(LB_RESERVED_PREFIX)) {
|
|
@@ -52,6 +82,44 @@ export function createHub(pages) {
|
|
|
52
82
|
}
|
|
53
83
|
}
|
|
54
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Answers into response items, adding the kind and key each query
|
|
87
|
+
* declared. An answer that disagrees with its declaration is a mistake in
|
|
88
|
+
* the application rather than a case to handle, and is left out: a page is
|
|
89
|
+
* better off missing one query than showing the wrong shape for it.
|
|
90
|
+
*/
|
|
91
|
+
function items(page, data) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const [name, result] of Object.entries(data)) {
|
|
94
|
+
const query = pages[page]?.queries[name];
|
|
95
|
+
if (!query) {
|
|
96
|
+
console.warn(`loadbare: page '${page}' declares no query '${name}'`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const item = {
|
|
100
|
+
query: name,
|
|
101
|
+
kind: query.kind,
|
|
102
|
+
key: query.key,
|
|
103
|
+
};
|
|
104
|
+
if (query.kind === KIND_ROW) {
|
|
105
|
+
if (Array.isArray(result) || patches.has(result)) {
|
|
106
|
+
console.warn(`loadbare: query '${name}' is declared row but answered with ` +
|
|
107
|
+
`${Array.isArray(result) ? "rows" : "a patch"}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
item.row = result;
|
|
111
|
+
}
|
|
112
|
+
else if (Array.isArray(result)) {
|
|
113
|
+
item.rows = result;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
item.patch = result;
|
|
117
|
+
}
|
|
118
|
+
out.push(item);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** Run queries by name, each answering with all its rows or its row. */
|
|
55
123
|
async function run(page, names, ctx) {
|
|
56
124
|
const data = {};
|
|
57
125
|
for (const name of names) {
|
|
@@ -61,10 +129,7 @@ export function createHub(pages) {
|
|
|
61
129
|
continue;
|
|
62
130
|
}
|
|
63
131
|
const result = await query.run(ctx);
|
|
64
|
-
|
|
65
|
-
// the query rather than a case to handle. Refused here, because a page
|
|
66
|
-
// is better off missing one name than showing the wrong shape for it.
|
|
67
|
-
if (Array.isArray(result) !== (query.kind === "list")) {
|
|
132
|
+
if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {
|
|
68
133
|
console.warn(`loadbare: query '${name}' is declared ${query.kind} but answered ` +
|
|
69
134
|
`with ${Array.isArray(result) ? "rows" : "one row"}`);
|
|
70
135
|
continue;
|
|
@@ -74,41 +139,79 @@ export function createHub(pages) {
|
|
|
74
139
|
return data;
|
|
75
140
|
}
|
|
76
141
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
142
|
+
* Run a handler, then its refresh set against the same context, laying
|
|
143
|
+
* what the handler itself returned over the refreshed queries — the
|
|
144
|
+
* narrower answer wins because it knows what actually changed.
|
|
145
|
+
*
|
|
146
|
+
* A handler that returned `url()` answers with the `lb-url` item alone.
|
|
147
|
+
* Loading the page there needs a context built at the new URL, and
|
|
148
|
+
* building one is the caller's.
|
|
81
149
|
*/
|
|
82
|
-
async function settle(page,
|
|
83
|
-
if (!
|
|
150
|
+
async function settle(page, handler, request, ctx, notFound) {
|
|
151
|
+
if (!handler) {
|
|
84
152
|
console.warn(notFound);
|
|
85
|
-
return
|
|
153
|
+
return [];
|
|
86
154
|
}
|
|
87
|
-
const stated = await
|
|
88
|
-
|
|
155
|
+
const { [URL_QUERY]: moved, ...stated } = (await handler.run(ctx, request)) ?? {};
|
|
156
|
+
if (moved !== undefined) {
|
|
157
|
+
const refused = refusal(moved);
|
|
158
|
+
if (refused === undefined) {
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
query: URL_QUERY,
|
|
162
|
+
kind: KIND_ROW,
|
|
163
|
+
key: URL_COLUMN_PATH,
|
|
164
|
+
row: moved,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
// The handler has already run, so the page still hears about it, by
|
|
169
|
+
// its refresh set, as if no row for lb-url had been returned.
|
|
170
|
+
console.warn(`loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`);
|
|
171
|
+
}
|
|
172
|
+
return items(page, {
|
|
173
|
+
...(await run(page, handler.refresh, ctx)),
|
|
174
|
+
...stated,
|
|
175
|
+
});
|
|
89
176
|
}
|
|
90
177
|
return {
|
|
91
178
|
async dataForPage(page, ctx) {
|
|
92
179
|
const entry = pages[page];
|
|
93
180
|
if (!entry) {
|
|
94
181
|
console.warn(`loadbare: no page '${page}'`);
|
|
95
|
-
return
|
|
182
|
+
return [];
|
|
96
183
|
}
|
|
97
184
|
await entry.requests.onPageEnter?.(ctx);
|
|
98
|
-
return run(page, Object.keys(entry.queries), ctx);
|
|
99
|
-
},
|
|
100
|
-
runAction(page, name, where, ctx) {
|
|
101
|
-
return settle(page, pages[page]?.requests.actions?.[name], where, ctx, `loadbare: page '${page}' declares no action '${name}'`);
|
|
102
|
-
},
|
|
103
|
-
runRowDelete(page, where, ctx) {
|
|
104
|
-
return settle(page, pages[page]?.requests.crud?.[where.list]?.rowDelete, { key: where.key }, ctx, `loadbare: page '${page}' declares no rowDelete for '${where.list}'`);
|
|
185
|
+
return items(page, await run(page, Object.keys(entry.queries), ctx));
|
|
105
186
|
},
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
187
|
+
runRequest(page, request, ctx) {
|
|
188
|
+
const { name, ...fields } = request;
|
|
189
|
+
if (isRowRequest(name)) {
|
|
190
|
+
const query = fields.query ?? "";
|
|
191
|
+
return settle(page, pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]], fields, ctx, `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`);
|
|
192
|
+
}
|
|
193
|
+
return settle(page, pages[page]?.requests.handlers?.[name], fields, ctx, `loadbare: page '${page}' declares no request '${name}'`);
|
|
111
194
|
},
|
|
112
195
|
};
|
|
113
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Why a returned row for `lb-url` cannot be used, or nothing when it can.
|
|
199
|
+
*
|
|
200
|
+
* A value is a string, as a control's is and a URL's is. At least one column
|
|
201
|
+
* is named: a row naming none changes nothing.
|
|
202
|
+
*/
|
|
203
|
+
function refusal(columns) {
|
|
204
|
+
if (typeof columns !== "object" ||
|
|
205
|
+
columns === null ||
|
|
206
|
+
Array.isArray(columns)) {
|
|
207
|
+
return "that is not a row";
|
|
208
|
+
}
|
|
209
|
+
const values = Object.values(columns);
|
|
210
|
+
if (values.length === 0)
|
|
211
|
+
return "naming no column";
|
|
212
|
+
if (!values.every((v) => typeof v === "string")) {
|
|
213
|
+
return `holding a value that is not a string; write url({ name: "..." })`;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
114
217
|
//# sourceMappingURL=lb-server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Hub on the server. It ships no\n// HTTP server, no router, and no data layer.\n\nimport type { Patch, Row, HubData, HubResult } from \"../core/lb-types.js\";\nimport { LB_RESERVED_PREFIX } from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Hub declares it empty and never reads it. An application fills it in by\n * declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n * The page's query parms are the other expected member: a query that reads\n * one off the context takes no argument, and re-runs under `refresh` like\n * any other.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: the request context in, one result out.\n *\n * Cardinality is a property of the name rather than of any one answer, so it\n * is declared here and never inferred from what comes back. One name answers\n * with one shape, always. A page that wants the roster once as a single row\n * and once as a set declares two queries.\n *\n * Write one with `row()` or `list()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: \"row\" | \"list\";\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row. */\nexport function row(run: (ctx: HubContext) => Row | Promise<Row>): Query {\n return { kind: \"row\", run };\n}\n\n/**\n * A query that answers with the entire set, and therefore also the order. A\n * list reconciles to exactly this: a row whose key is not here is gone.\n *\n * There is no wrapper around the array, because the declaration already said\n * this name answers with rows.\n */\nexport function list(run: (ctx: HubContext) => Row[] | Promise<Row[]>): Query {\n return { kind: \"list\", run };\n}\n\n/**\n * Only what changed, returned from a `crud` run rather than from a query.\n * Rows named here arrive or are updated, keys in `drop` are gone, and\n * everything unnamed is left alone — its contents, and its place in whatever\n * order the widget is keeping.\n *\n * `Array.isArray` is what tells a patch from a whole set, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: string[] }): Patch {\n return { ...change };\n}\n\n/** The shape of a `<name>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * Where the interaction happened, in the binding vocabulary, plus the one\n * value a control may carry.\n *\n * The browser fills these from attributes it already has. It never names a\n * function — only a name the page declared — which is what keeps this from\n * being an RPC endpoint. A value may ride along because a `<select>` has one\n * and there is nowhere else to put it; it is a string from a control, not an\n * argument list.\n */\nexport interface Where {\n list?: string;\n row?: string;\n key?: string;\n cell?: string;\n value?: string;\n}\n\n/**\n * What an action does, and which queries must re-run once it has.\n *\n * `run` may also return results of its own, which are laid over the refreshed\n * ones. That is how a patch reaches the browser: a query answers for its\n * whole set and cannot know why it was re-run, but the action knows exactly\n * what it changed and can say only that.\n */\nexport interface Action {\n run: (\n ctx: HubContext,\n where: Where,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\n/**\n * One CRUD operation on a declared query. Same shape as `Action` — run, then\n * refresh — but `where` carries only what that operation is typed to carry\n * on the wire, rather than the general `Where`.\n */\nexport interface CrudOp<W> {\n run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\n/** `values` holds only the columns being set; see `Hub.runRowUpdate`. */\nexport type RowUpdateOp = CrudOp<{\n key: string;\n values: Record<string, string>;\n}>;\n\n/**\n * The CRUD operations declared for one list, keyed by its name in\n * `Requests.crud`. A name with no entry here permits none of them — the wire\n * cannot reach anything the page has not published, exactly as for a named\n * action.\n *\n * Every key here is the reserved `lb-action` value with the prefix stripped\n * and the rest camel-cased, so the attribute, the wire field and this key\n * are one vocabulary. All three are list operations: each needs a key, and a\n * key exists only on a live row inside a list.\n */\nexport interface Crud {\n rowDelete?: RowDeleteOp;\n rowInsert?: RowInsertOp;\n rowUpdate?: RowUpdateOp;\n}\n\n/**\n * The shape of a `<name>.requests.ts` module.\n *\n * `onPageEnter` runs once when the page is entered, before any query. It\n * declares no refresh set: entering the page runs the whole query set\n * afterward, so whatever the hook changed is already in the response.\n *\n * `actions` names what this page may be asked to do. A name the page did not\n * declare is refused, so the wire cannot reach anything the page has not\n * published.\n *\n * `crud` is the same rule for the typed CRUD operations, keyed by the list\n * they operate on rather than by a declared name — there is nothing to name,\n * since the row's own binding says what it is.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n actions?: Record<string, Action>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a basename; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder populates this automatically from every\n * `.requests.ts`/`.queries.ts` pair it discovers — see docs/reference/builder.md,\n * \"Generating the server-side page registry\".\n */\nexport type Pages = Record<string, Page>;\n\nexport interface Hub {\n /** Entering a page: run its onPageEnter hook, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubData>;\n\n /**\n * An action: run what the page declared under that name, then the refresh\n * set declared with it.\n */\n runAction(\n page: string,\n name: string,\n where: Where,\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Drop one row: run the list's declared `rowDelete`, then its refresh set. */\n runRowDelete(\n page: string,\n where: { list: string; key: string },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /** Add one row: run the list's declared `rowInsert`, then its refresh set. */\n runRowInsert(\n page: string,\n where: { list: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n\n /**\n * Edit a row: run the list's declared `rowUpdate`, then its refresh set.\n * `values` holds only the columns being set — one from a widget that\n * commits a cell, several from a form — and a column absent from it is left\n * as it is, as an SQL UPDATE leaves it.\n */\n runRowUpdate(\n page: string,\n where: { list: string; key: string; values: Record<string, string> },\n ctx: HubContext,\n ): Promise<HubData>;\n}\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — the hub's\n // own query, the CRUD operations — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.actions ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n // Cardinality is declared, so an answer that disagrees is a mistake in\n // the query rather than a case to handle. Refused here, because a page\n // is better off missing one name than showing the wrong shape for it.\n if (Array.isArray(result) !== (query.kind === \"list\")) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Shared by every operation kind: run what was declared, then its refresh\n * set against the same context, laying what the operation itself stated\n * over the refreshed queries — the narrower answer wins because it is the\n * one that knows what actually changed.\n */\n async function settle<W>(\n page: string,\n declared: CrudOp<W> | Action | undefined,\n where: W,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubData> {\n if (!declared) {\n console.warn(notFound);\n return {};\n }\n const stated = await declared.run(ctx, where as never);\n return { ...(await run(page, declared.refresh, ctx)), ...(stated ?? {}) };\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return {};\n }\n await entry.requests.onPageEnter?.(ctx);\n return run(page, Object.keys(entry.queries), ctx);\n },\n\n runAction(page, name, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.actions?.[name],\n where,\n ctx,\n `loadbare: page '${page}' declares no action '${name}'`,\n );\n },\n\n runRowDelete(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowDelete,\n { key: where.key },\n ctx,\n `loadbare: page '${page}' declares no rowDelete for '${where.list}'`,\n );\n },\n\n runRowInsert(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowInsert,\n { values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowInsert for '${where.list}'`,\n );\n },\n\n runRowUpdate(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.rowUpdate,\n { key: where.key, values: where.values },\n ctx,\n `loadbare: page '${page}' declares no rowUpdate for '${where.list}'`,\n );\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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","sourcesContent":["// The server-side contract and the engine that runs it.\n//\n// This is the counterpart to core/lb-constants.ts: that file is the\n// vocabulary the browser writes, this one is the vocabulary the application\n// writes. The engine below is the whole of Loadbare on the server. It ships\n// no HTTP server, no router, and no data layer.\n\nimport type {\n HubData,\n HubRequest,\n HubResponse,\n HubResult,\n Kind,\n Patch,\n RequestFields,\n ResponseItem,\n Row,\n} from \"../core/lb-types.js\";\nimport { isRowRequest } from \"../core/lb-types.js\";\nimport {\n KIND_ROW,\n KIND_ROWS,\n LB_RESERVED_PREFIX,\n REQUEST_ROW_DELETE,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n URL_COLUMN_PATH,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\n\n/**\n * Whatever the application hands the engine for the duration of one request.\n *\n * Loadbare declares it empty and never reads it. An application fills it in\n * by declaration merging, once, anywhere in its own source:\n *\n * declare module \"@loadbare/app/server\" {\n * interface HubContext {\n * db: Db;\n * }\n * }\n *\n * That is why no type on this page takes a type parameter. The context is a\n * request-scoped handle — an authenticated database connection is the\n * expected case — so it is passed per call rather than held by the engine.\n * The page's query parms are the other expected member: a query that reads\n * one off the context takes no argument, and re-runs under `refresh` like\n * any other.\n */\nexport interface HubContext {}\n\n/**\n * A declared query: its kind, its key, and the run that answers it.\n *\n * Kind and key are properties of the name rather than of any one answer, so\n * they are declared here and never inferred from what comes back. The engine\n * sends both with every answer, so the markup repeats neither. Every query\n * has a key; an aggregate row answers with a constant one.\n *\n * Write one with `row()` or `rows()` below; nothing else builds one.\n */\nexport interface Query {\n readonly kind: Kind;\n readonly key: string;\n readonly run: (ctx: HubContext) => HubResult | Promise<HubResult>;\n}\n\n/** A query that answers with one row, identified by its `key` column. */\nexport function row(\n key: string,\n run: (ctx: HubContext) => Row | Promise<Row>,\n): Query {\n return { kind: KIND_ROW, key, run };\n}\n\n/**\n * A query that answers with all its rows, and therefore also their order.\n * Rows land by their `key` column: a row whose key is not in the answer is\n * gone.\n */\nexport function rows(\n key: string,\n run: (ctx: HubContext) => Row[] | Promise<Row[]>,\n): Query {\n return { kind: KIND_ROWS, key, run };\n}\n\n/**\n * Only what changed, returned from a handler for a `rows` query. Rows named\n * here are added or updated, keys in `drop` are removed, and everything\n * unnamed is left alone — its contents, and its place in whatever order the\n * page is keeping.\n *\n * `Array.isArray` is what tells a patch from all rows, so a patch needs no\n * marker of its own and no column name is reserved to carry one.\n */\nexport function patch(change: { rows?: Row[]; drop?: unknown[] }): Patch {\n const made = { ...change };\n patches.add(made);\n return made;\n}\n\n/**\n * Every object `patch()` made. A patch and a row are both plain objects, so\n * this is how the engine refuses a patch answering a `row` query.\n */\nconst patches = new WeakSet<object>();\n\n/**\n * A new row for `lb-url`, returned from a handler when only the handler can\n * know where the page belongs: the key of a row it inserted, or the absence\n * of one it deleted.\n *\n * A column other than `lb-path` is a query parm: set, or taken out when its\n * value is empty. With `lb-path` naming another page, the page is entered\n * with only the parms named here; otherwise every other parm is kept.\n *\n * The page then loads at the new URL in the same round trip, as a cold load\n * of it would, so the refresh set is not run and whatever else the handler\n * returned is dropped: both were answers for the URL the page is leaving.\n * This is Post/Redirect/Get without the redirect.\n */\nexport function url(columns: Record<string, string>): HubData {\n return { [URL_QUERY]: { ...columns } };\n}\n\n/** The shape of a `<stub>.queries.ts` module. */\nexport type Queries = Record<string, Query>;\n\n/**\n * What a handler does, and which queries re-run once it has.\n *\n * `run` may also return answers of its own, which are laid over the\n * refreshed ones. That is how a patch reaches the browser: a query answers\n * for all its rows and cannot know why it re-ran, but the handler knows\n * exactly what it changed and can say only that.\n */\nexport interface Handler<R = RequestFields> {\n run: (\n ctx: HubContext,\n request: R,\n ) => void | HubData | Promise<void | HubData>;\n refresh: string[];\n}\n\nexport type RowInsertHandler = Handler<{\n key?: string;\n values: Record<string, string>;\n}>;\n/** `values` holds only the columns being set, as an SQL UPDATE sets them. */\nexport type RowUpdateHandler = Handler<{\n key: string;\n values: Record<string, string>;\n}>;\nexport type RowDeleteHandler = Handler<{ key: string }>;\n\n/**\n * What the requests Loadbare provides do to one query, keyed by its name in\n * `Requests.crud`. A query with no entry here permits none of them: the wire\n * cannot reach anything the page has not published.\n *\n * Every key is the request name with the prefix stripped and the rest\n * camel-cased: `lb-row-insert` runs `rowInsert`.\n */\nexport interface Crud {\n rowInsert?: RowInsertHandler;\n rowUpdate?: RowUpdateHandler;\n rowDelete?: RowDeleteHandler;\n}\n\n/**\n * The shape of a `<stub>.requests.ts` module.\n *\n * `onPageEnter` runs when the page loads, before its queries. It declares no\n * refresh set: the page's queries all run afterward, so whatever it changed\n * is already in the response.\n *\n * `handlers` holds the page's declared requests, by name. A name the page\n * did not declare is refused, so the wire cannot reach anything the page has\n * not published.\n *\n * `crud` holds what the requests Loadbare provides run, by query name.\n */\nexport interface Requests {\n onPageEnter?: (ctx: HubContext) => void | Promise<void>;\n handlers?: Record<string, Handler>;\n crud?: Record<string, Crud>;\n}\n\n/** A page is three files sharing a stub; two of them are these. */\nexport interface Page {\n queries: Queries;\n requests: Requests;\n}\n\n/**\n * The page registry. The builder writes it from every `.requests.ts` and\n * `.queries.ts` it discovers — see docs/reference/builder.md.\n */\nexport type Pages = Record<string, Page>;\n\n/**\n * The engine. Both calls answer with response items. A request whose\n * handler returned `url()` answers with the `lb-url` item alone: the caller\n * builds a context at the new URL, calls `dataForPage` with it, and sends\n * the load beside the item. `hubRoutes` does exactly that.\n */\nexport interface Hub {\n /** A page load: its `onPageEnter`, then all of its queries. */\n dataForPage(page: string, ctx: HubContext): Promise<HubResponse>;\n\n /** A request: run the handler its name picks, then the refresh set. */\n runRequest(\n page: string,\n request: HubRequest,\n ctx: HubContext,\n ): Promise<HubResponse>;\n}\n\n/** The camel-cased `Crud` key of a request Loadbare provides. */\nconst CRUD_KEYS: Record<string, keyof Crud> = {\n [REQUEST_ROW_INSERT]: \"rowInsert\",\n [REQUEST_ROW_UPDATE]: \"rowUpdate\",\n [REQUEST_ROW_DELETE]: \"rowDelete\",\n};\n\n/**\n * Build the engine over a set of pages. The pages are fixed at startup; the\n * context is not, and arrives with each call.\n */\nexport function createHub(pages: Pages): Hub {\n // Names beginning with the reserved prefix are Loadbare's — its own query,\n // the requests it provides — so an application cannot declare one.\n // Refused at startup, because a name is a fact about the page and not\n // about any request.\n for (const [page, entry] of Object.entries(pages)) {\n const declared = [\n ...Object.keys(entry.queries),\n ...Object.keys(entry.requests.handlers ?? {}),\n ...Object.keys(entry.requests.crud ?? {}),\n ];\n for (const name of declared) {\n if (name.startsWith(LB_RESERVED_PREFIX)) {\n throw new Error(\n `loadbare: page '${page}' declares '${name}', but names ` +\n `beginning with '${LB_RESERVED_PREFIX}' are reserved`,\n );\n }\n }\n }\n\n /**\n * Answers into response items, adding the kind and key each query\n * declared. An answer that disagrees with its declaration is a mistake in\n * the application rather than a case to handle, and is left out: a page is\n * better off missing one query than showing the wrong shape for it.\n */\n function items(page: string, data: HubData): HubResponse {\n const out: ResponseItem[] = [];\n for (const [name, result] of Object.entries(data)) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const item: ResponseItem = {\n query: name,\n kind: query.kind,\n key: query.key,\n };\n if (query.kind === KIND_ROW) {\n if (Array.isArray(result) || patches.has(result)) {\n console.warn(\n `loadbare: query '${name}' is declared row but answered with ` +\n `${Array.isArray(result) ? \"rows\" : \"a patch\"}`,\n );\n continue;\n }\n item.row = result as Row;\n } else if (Array.isArray(result)) {\n item.rows = result;\n } else {\n item.patch = result as Patch;\n }\n out.push(item);\n }\n return out;\n }\n\n /** Run queries by name, each answering with all its rows or its row. */\n async function run(\n page: string,\n names: string[],\n ctx: HubContext,\n ): Promise<HubData> {\n const data: HubData = {};\n for (const name of names) {\n const query = pages[page]?.queries[name];\n if (!query) {\n console.warn(`loadbare: page '${page}' declares no query '${name}'`);\n continue;\n }\n const result = await query.run(ctx);\n if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {\n console.warn(\n `loadbare: query '${name}' is declared ${query.kind} but answered ` +\n `with ${Array.isArray(result) ? \"rows\" : \"one row\"}`,\n );\n continue;\n }\n data[name] = result;\n }\n return data;\n }\n\n /**\n * Run a handler, then its refresh set against the same context, laying\n * what the handler itself returned over the refreshed queries — the\n * narrower answer wins because it knows what actually changed.\n *\n * A handler that returned `url()` answers with the `lb-url` item alone.\n * Loading the page there needs a context built at the new URL, and\n * building one is the caller's.\n */\n async function settle(\n page: string,\n handler: Handler<never> | undefined,\n request: RequestFields,\n ctx: HubContext,\n notFound: string,\n ): Promise<HubResponse> {\n if (!handler) {\n console.warn(notFound);\n return [];\n }\n const { [URL_QUERY]: moved, ...stated } =\n (await handler.run(ctx, request as never)) ?? {};\n if (moved !== undefined) {\n const refused = refusal(moved);\n if (refused === undefined) {\n return [\n {\n query: URL_QUERY,\n kind: KIND_ROW,\n key: URL_COLUMN_PATH,\n row: moved as Row,\n },\n ];\n }\n // The handler has already run, so the page still hears about it, by\n // its refresh set, as if no row for lb-url had been returned.\n console.warn(\n `loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`,\n );\n }\n return items(page, {\n ...(await run(page, handler.refresh, ctx)),\n ...stated,\n });\n }\n\n return {\n async dataForPage(page, ctx) {\n const entry = pages[page];\n if (!entry) {\n console.warn(`loadbare: no page '${page}'`);\n return [];\n }\n await entry.requests.onPageEnter?.(ctx);\n return items(page, await run(page, Object.keys(entry.queries), ctx));\n },\n\n runRequest(page, request, ctx) {\n const { name, ...fields } = request;\n if (isRowRequest(name)) {\n const query = fields.query ?? \"\";\n return settle(\n page,\n pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]!] as\n Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`,\n );\n }\n return settle(\n page,\n pages[page]?.requests.handlers?.[name] as Handler<never> | undefined,\n fields,\n ctx,\n `loadbare: page '${page}' declares no request '${name}'`,\n );\n },\n };\n}\n\n/**\n * Why a returned row for `lb-url` cannot be used, or nothing when it can.\n *\n * A value is a string, as a control's is and a URL's is. At least one column\n * is named: a row naming none changes nothing.\n */\nfunction refusal(columns: unknown): string | undefined {\n if (\n typeof columns !== \"object\" ||\n columns === null ||\n Array.isArray(columns)\n ) {\n return \"that is not a row\";\n }\n const values = Object.values(columns);\n if (values.length === 0) return \"naming no column\";\n if (!values.every((v) => typeof v === \"string\")) {\n return `holding a value that is not a string; write url({ name: \"...\" })`;\n }\n return undefined;\n}\n"]}
|