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