@loadbare/app 0.8.2 → 0.9.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/dist/core/lb-constants.d.ts +1 -0
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +10 -0
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +11 -0
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +20 -0
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +50 -8
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts +17 -10
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +58 -29
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +23 -0
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +53 -3
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +70 -9
- package/docs/reference/data-binding.md +6 -1
- package/docs/reference/page-files.md +39 -0
- package/docs/reference/server.md +12 -6
- package/docs/roadmap.md +12 -0
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +16 -5
- package/skills/loadbare-app/references/TECHREF-1.0.md +70 -9
- package/skills/loadbare-app/references/data-binding.md +6 -1
- package/skills/loadbare-app/references/page-files.md +39 -0
- package/skills/loadbare-app/references/server.md +12 -6
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
* POST /lb/<page>?<query string>
|
|
12
12
|
* an empty body asks for the page's whole query
|
|
13
13
|
* set; a body carrying an action runs that action
|
|
14
|
-
* and answers with its refresh set
|
|
14
|
+
* and answers with its refresh set, or with the
|
|
15
|
+
* page loaded at the query parms it wrote
|
|
15
16
|
*
|
|
16
17
|
* The page rides on the path rather than in the body, which is what lets the
|
|
17
18
|
* operation set stay closed. The query string is the one the browser is
|
|
18
|
-
* showing, verbatim,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* no page name is reserved.
|
|
19
|
+
* showing, verbatim, and `contextFor` is handed its parms. What a parm means
|
|
20
|
+
* is the application's: nothing here reads one. Every hub call is a POST, so
|
|
21
|
+
* an application's own GET routes never collide with this one and no page
|
|
22
|
+
* name is reserved.
|
|
23
23
|
*/
|
|
24
24
|
import express from "express";
|
|
25
|
-
import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, } from "../core/lb-constants.js";
|
|
26
|
-
import { isOperation } from "../core/lb-types.js";
|
|
25
|
+
import { ACTION_ROW_DELETE, ACTION_ROW_INSERT, ACTION_ROW_UPDATE, LB_ENDPOINT, QUERY_PARMS_ROW, } from "../core/lb-constants.js";
|
|
26
|
+
import { isOperation, withQueryParms, } from "../core/lb-types.js";
|
|
27
27
|
export function hubRoutes(hub, contextFor) {
|
|
28
28
|
const router = express.Router();
|
|
29
29
|
// Mounted here rather than on the app so that Hub's need for a parsed
|
|
@@ -34,7 +34,10 @@ export function hubRoutes(hub, contextFor) {
|
|
|
34
34
|
// gathered whole.
|
|
35
35
|
const page = req.params.page.join("/");
|
|
36
36
|
const request = req.body;
|
|
37
|
-
|
|
37
|
+
// Parsed here rather than taken from req.query, so that the parms a
|
|
38
|
+
// context is built from are the same kind of thing on both calls.
|
|
39
|
+
const parms = new URL(req.originalUrl, "http://localhost").searchParams;
|
|
40
|
+
const ctx = contextFor(req, parms);
|
|
38
41
|
// No body at all is the page load: the browser is asking for this
|
|
39
42
|
// page's whole query set, which is every request it makes that names no
|
|
40
43
|
// action. A body that carries something but not an action is malformed.
|
|
@@ -51,39 +54,65 @@ export function hubRoutes(hub, contextFor) {
|
|
|
51
54
|
// op) says "this request is invalid" or hits an unexpected failure —
|
|
52
55
|
// it resolves to a real response rather than an unhandled rejection, so
|
|
53
56
|
// the browser side has something to catch.
|
|
57
|
+
let data;
|
|
54
58
|
try {
|
|
55
59
|
// The reserved prefix is the whole of the discriminant: a name so
|
|
56
60
|
// prefixed is one of the three operations, and anything else is a name
|
|
57
61
|
// the page declared. Nothing else tells them apart, here or anywhere.
|
|
58
62
|
if (!isOperation(request)) {
|
|
59
63
|
const { action, list, row, key, cell, value } = request;
|
|
60
|
-
|
|
61
|
-
return;
|
|
64
|
+
data = await hub.runAction(page, action, { list, row, key, cell, value }, ctx);
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
else {
|
|
67
|
+
switch (request.action) {
|
|
68
|
+
case ACTION_ROW_DELETE: {
|
|
69
|
+
const { list, key } = request;
|
|
70
|
+
data = await hub.runRowDelete(page, { list, key }, ctx);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case ACTION_ROW_INSERT: {
|
|
74
|
+
const { list, values } = request;
|
|
75
|
+
data = await hub.runRowInsert(page, { list, values }, ctx);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case ACTION_ROW_UPDATE: {
|
|
79
|
+
const { list, key, values } = request;
|
|
80
|
+
data = await hub.runRowUpdate(page, { list, key, values }, ctx);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
console.warn(`loadbare: operation '${request.action}' not implemented`);
|
|
85
|
+
res.status(400).json({});
|
|
86
|
+
return;
|
|
68
87
|
}
|
|
69
|
-
case ACTION_ROW_INSERT: {
|
|
70
|
-
const { list, values } = request;
|
|
71
|
-
res.json(await hub.runRowInsert(page, { list, values }, ctx));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
case ACTION_ROW_UPDATE: {
|
|
75
|
-
const { list, key, values } = request;
|
|
76
|
-
res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
default:
|
|
80
|
-
console.warn(`loadbare: operation '${request.action}' not implemented`);
|
|
81
|
-
res.status(400).json({});
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
catch (err) {
|
|
85
91
|
console.error(`loadbare: '${request.action}' threw`, err);
|
|
86
92
|
res.status(500).json({});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const written = data[QUERY_PARMS_ROW];
|
|
96
|
+
if (written === undefined) {
|
|
97
|
+
res.json(data);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// The request changed the query parms, so the page loads at them, as a
|
|
101
|
+
// cold load of the new URL would. The write has already happened: from
|
|
102
|
+
// here a failure must not read as the write failing, or the user would
|
|
103
|
+
// send it again. The parms go back alone instead, and the hub, writing
|
|
104
|
+
// them into the URL with nothing to land, loads the page itself.
|
|
105
|
+
try {
|
|
106
|
+
const next = contextFor(req, withQueryParms(parms, written));
|
|
107
|
+
res.json({
|
|
108
|
+
...(await hub.dataForPage(page, next)),
|
|
109
|
+
[QUERY_PARMS_ROW]: written,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error(`loadbare: '${request.action}' wrote query parms, and loading ` +
|
|
114
|
+
`'${page}' at them threw`, err);
|
|
115
|
+
res.json({ [QUERY_PARMS_ROW]: written });
|
|
87
116
|
}
|
|
88
117
|
});
|
|
89
118
|
// A body that never parsed is refused like any other malformed request,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Express adapter — the standard hand-off from an Express router to a\n * Hub built by createHub(). Every Hub application wires this same router,\n * which is why it ships from here rather than being copied into each one.\n *\n * Express itself is a peer dependency: nothing outside this file imports it,\n * so an application that hosts Hub some other way never pays for it.\n *\n * One endpoint, and no second:\n *\n * POST /lb/<page>?<query string>\n * an empty body asks for the page's whole query\n * set; a body carrying an action runs that action\n * and answers with its refresh set\n *\n * The page rides on the path rather than in the body, which is what lets the\n * operation set stay closed. The query string is the one the browser is\n * showing, verbatim, so `req.query` holds the page's query parms and\n * `contextFor` reads them there like any other part of the request. What a\n * parm means is the application's: nothing here reads one. Every hub call is\n * a POST, so an application's own GET routes never collide with this one and\n * no page name is reserved.\n */\n\nimport express, {\n type NextFunction,\n type Request,\n type Response,\n type Router,\n} from \"express\";\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n LB_ENDPOINT,\n} from \"../core/lb-constants.js\";\nimport { isOperation, type HubRequest } from \"../core/lb-types.js\";\nimport type { Hub, HubContext } from \"./lb-server.js\";\n\n/**\n * Building the context is the application's job — it is where an\n * authenticated database handle comes from — so it is injected rather than\n * assumed. Whatever established identity has already run by the time this\n * router is reached; where that happens is the application's ordering\n * decision, not Hub's.\n *\n * The query parms arrive here too, on `req.query`. They are user input like\n * any other, typed into an address bar or mailed in a link, and are\n * validated where they are read.\n */\nexport type ContextFor = (req: Request) => HubContext;\n\nexport function hubRoutes(hub: Hub, contextFor: ContextFor): Router {\n const router = express.Router();\n\n // Mounted here rather than on the app so that Hub's need for a parsed\n // body is stated where the routes that need it are.\n router.use(express.json());\n\n router.post(`${LB_ENDPOINT}/*page`, async (req, res) => {\n // A page name may hold a slash, so the path below the endpoint is\n // gathered whole.\n const page = (req.params.page as string[]).join(\"/\");\n const request = req.body as HubRequest | undefined;\n const ctx = contextFor(req);\n\n // No body at all is the page load: the browser is asking for this\n // page's whole query set, which is every request it makes that names no\n // action. A body that carries something but not an action is malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n res.json(await hub.dataForPage(page, ctx));\n return;\n }\n\n if (!request.action) {\n console.warn(`loadbare: request without an action`);\n res.status(400).json({});\n return;\n }\n\n // A thrown error is how application code (a query, an action, a CRUD\n // op) says \"this request is invalid\" or hits an unexpected failure —\n // it resolves to a real response rather than an unhandled rejection, so\n // the browser side has something to catch.\n try {\n // The reserved prefix is the whole of the discriminant: a name so\n // prefixed is one of the three operations, and anything else is a name\n // the page declared. Nothing else tells them apart, here or anywhere.\n if (!isOperation(request)) {\n const { action, list, row, key, cell, value } = request;\n res.json(\n await hub.runAction(\n page,\n action,\n { list, row, key, cell, value },\n ctx,\n ),\n );\n return;\n }\n switch (request.action) {\n case ACTION_ROW_DELETE: {\n const { list, key } = request;\n res.json(await hub.runRowDelete(page, { list, key }, ctx));\n return;\n }\n case ACTION_ROW_INSERT: {\n const { list, values } = request;\n res.json(await hub.runRowInsert(page, { list, values }, ctx));\n return;\n }\n case ACTION_ROW_UPDATE: {\n const { list, key, values } = request;\n res.json(await hub.runRowUpdate(page, { list, key, values }, ctx));\n return;\n }\n default:\n console.warn(\n `loadbare: operation '${(request as HubRequest).action}' not implemented`,\n );\n res.status(400).json({});\n }\n } catch (err) {\n console.error(`loadbare: '${request.action}' threw`, err);\n res.status(500).json({});\n }\n });\n\n // A body that never parsed is refused like any other malformed request,\n // rather than falling through to the application's error handler, which\n // would print a stack and call it a server failure. Only for the hub's own\n // path: express.json() above sees every request, and a bad body sent to one\n // of the application's routes is the application's to answer.\n router.use(\n `${LB_ENDPOINT}/*page`,\n (err: unknown, _req: Request, res: Response, next: NextFunction) => {\n if ((err as { type?: string }).type !== \"entity.parse.failed\") {\n next(err);\n return;\n }\n console.warn(`loadbare: request body is not JSON`);\n res.status(400).json({});\n },\n );\n\n return router;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Express adapter — the standard hand-off from an Express router to a\n * Hub built by createHub(). Every Hub application wires this same router,\n * which is why it ships from here rather than being copied into each one.\n *\n * Express itself is a peer dependency: nothing outside this file imports it,\n * so an application that hosts Hub some other way never pays for it.\n *\n * One endpoint, and no second:\n *\n * POST /lb/<page>?<query string>\n * an empty body asks for the page's whole query\n * set; a body carrying an action runs that action\n * and answers with its refresh set, or with the\n * page loaded at the query parms it wrote\n *\n * The page rides on the path rather than in the body, which is what lets the\n * operation set stay closed. The query string is the one the browser is\n * showing, verbatim, and `contextFor` is handed its parms. What a parm means\n * is the application's: nothing here reads one. Every hub call is a POST, so\n * an application's own GET routes never collide with this one and no page\n * name is reserved.\n */\n\nimport express, {\n type NextFunction,\n type Request,\n type Response,\n type Router,\n} from \"express\";\nimport {\n ACTION_ROW_DELETE,\n ACTION_ROW_INSERT,\n ACTION_ROW_UPDATE,\n LB_ENDPOINT,\n QUERY_PARMS_ROW,\n} from \"../core/lb-constants.js\";\nimport {\n isOperation,\n withQueryParms,\n type HubData,\n type HubRequest,\n} from \"../core/lb-types.js\";\nimport type { Hub, HubContext } from \"./lb-server.js\";\n\n/**\n * Building the context is the application's job — it is where an\n * authenticated database handle comes from — so it is injected rather than\n * assumed. Whatever established identity has already run by the time this\n * router is reached; where that happens is the application's ordering\n * decision, not Hub's.\n *\n * The query parms arrive as `parms`, and are read there rather than off\n * `req.query`. The two agree except after a request that wrote parms: the\n * page is then loaded at the new query string in the same round trip, and\n * this is called a second time for that load, with the same `req` and the\n * new `parms`. So it must be safe to call twice for one request, and a\n * write must be visible to the second context by the time its `run`\n * returns.\n *\n * Parms are user input like any other, typed into an address bar or mailed\n * in a link, and are validated where they are read.\n */\nexport type ContextFor = (req: Request, parms: URLSearchParams) => HubContext;\n\nexport function hubRoutes(hub: Hub, contextFor: ContextFor): Router {\n const router = express.Router();\n\n // Mounted here rather than on the app so that Hub's need for a parsed\n // body is stated where the routes that need it are.\n router.use(express.json());\n\n router.post(`${LB_ENDPOINT}/*page`, async (req, res) => {\n // A page name may hold a slash, so the path below the endpoint is\n // gathered whole.\n const page = (req.params.page as string[]).join(\"/\");\n const request = req.body as HubRequest | undefined;\n // Parsed here rather than taken from req.query, so that the parms a\n // context is built from are the same kind of thing on both calls.\n const parms = new URL(req.originalUrl, \"http://localhost\").searchParams;\n const ctx = contextFor(req, parms);\n\n // No body at all is the page load: the browser is asking for this\n // page's whole query set, which is every request it makes that names no\n // action. A body that carries something but not an action is malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n res.json(await hub.dataForPage(page, ctx));\n return;\n }\n\n if (!request.action) {\n console.warn(`loadbare: request without an action`);\n res.status(400).json({});\n return;\n }\n\n // A thrown error is how application code (a query, an action, a CRUD\n // op) says \"this request is invalid\" or hits an unexpected failure —\n // it resolves to a real response rather than an unhandled rejection, so\n // the browser side has something to catch.\n let data: HubData;\n try {\n // The reserved prefix is the whole of the discriminant: a name so\n // prefixed is one of the three operations, and anything else is a name\n // the page declared. Nothing else tells them apart, here or anywhere.\n if (!isOperation(request)) {\n const { action, list, row, key, cell, value } = request;\n data = await hub.runAction(\n page,\n action,\n { list, row, key, cell, value },\n ctx,\n );\n } else {\n switch (request.action) {\n case ACTION_ROW_DELETE: {\n const { list, key } = request;\n data = await hub.runRowDelete(page, { list, key }, ctx);\n break;\n }\n case ACTION_ROW_INSERT: {\n const { list, values } = request;\n data = await hub.runRowInsert(page, { list, values }, ctx);\n break;\n }\n case ACTION_ROW_UPDATE: {\n const { list, key, values } = request;\n data = await hub.runRowUpdate(page, { list, key, values }, ctx);\n break;\n }\n default:\n console.warn(\n `loadbare: operation '${(request as HubRequest).action}' not implemented`,\n );\n res.status(400).json({});\n return;\n }\n }\n } catch (err) {\n console.error(`loadbare: '${request.action}' threw`, err);\n res.status(500).json({});\n return;\n }\n\n const written = data[QUERY_PARMS_ROW] as Record<string, string> | undefined;\n if (written === undefined) {\n res.json(data);\n return;\n }\n // The request changed the query parms, so the page loads at them, as a\n // cold load of the new URL would. The write has already happened: from\n // here a failure must not read as the write failing, or the user would\n // send it again. The parms go back alone instead, and the hub, writing\n // them into the URL with nothing to land, loads the page itself.\n try {\n const next = contextFor(req, withQueryParms(parms, written));\n res.json({\n ...(await hub.dataForPage(page, next)),\n [QUERY_PARMS_ROW]: written,\n });\n } catch (err) {\n console.error(\n `loadbare: '${request.action}' wrote query parms, and loading ` +\n `'${page}' at them threw`,\n err,\n );\n res.json({ [QUERY_PARMS_ROW]: written });\n }\n });\n\n // A body that never parsed is refused like any other malformed request,\n // rather than falling through to the application's error handler, which\n // would print a stack and call it a server failure. Only for the hub's own\n // path: express.json() above sees every request, and a bad body sent to one\n // of the application's routes is the application's to answer.\n router.use(\n `${LB_ENDPOINT}/*page`,\n (err: unknown, _req: Request, res: Response, next: NextFunction) => {\n if ((err as { type?: string }).type !== \"entity.parse.failed\") {\n next(err);\n return;\n }\n console.warn(`loadbare: request body is not JSON`);\n res.status(400).json({});\n },\n );\n\n return router;\n}\n"]}
|
|
@@ -57,6 +57,21 @@ export declare function patch(change: {
|
|
|
57
57
|
rows?: Row[];
|
|
58
58
|
drop?: string[];
|
|
59
59
|
}): Patch;
|
|
60
|
+
/**
|
|
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.
|
|
65
|
+
*
|
|
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.
|
|
71
|
+
*
|
|
72
|
+
* Only parms. The path is never the server's to change.
|
|
73
|
+
*/
|
|
74
|
+
export declare function queryParms(parms: Record<string, string>): HubData;
|
|
60
75
|
/** The shape of a `<name>.queries.ts` module. */
|
|
61
76
|
export type Queries = Record<string, Query>;
|
|
62
77
|
/**
|
|
@@ -155,6 +170,14 @@ export interface Page {
|
|
|
155
170
|
* "Generating the server-side page registry".
|
|
156
171
|
*/
|
|
157
172
|
export type Pages = Record<string, Page>;
|
|
173
|
+
/**
|
|
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.
|
|
180
|
+
*/
|
|
158
181
|
export interface Hub {
|
|
159
182
|
/** Entering a page: run its onPageEnter hook, then all of its queries. */
|
|
160
183
|
dataForPage(page: string, ctx: HubContext): Promise<HubData>;
|
|
@@ -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":"AAOA,OAAO,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAG1E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,UAAU;CAAG;AAE9B;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACnE;AAED,yCAAyC;AACzC,wBAAgB,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAEvE;AAED;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,KAAK,CAE5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,KAAK,CAEtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAEjE;AAED,iDAAiD;AACjD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE5C;;;;;;;;;GASG;AACH,MAAM,WAAW,KAAK;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,CACH,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,KAAK,KACT,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,GAAG,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAClD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AACrE,yEAAyE;AACzE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,WAAW,IAAI;IACnB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ;IACvB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,WAAW,IAAI;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,WAAW,GAAG;IAClB,0EAA0E;IAC1E,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7D;;;OAGG;IACH,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB,+EAA+E;IAC/E,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EACpC,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB,8EAA8E;IAC9E,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACvD,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB;;;;;OAKG;IACH,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACpE,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,GAAG,CAyI3C"}
|
package/dist/server/lb-server.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// vocabulary the browser writes, this one is the vocabulary the application
|
|
5
5
|
// writes. The engine below is the whole of Hub on the server. It ships no
|
|
6
6
|
// HTTP server, no router, and no data layer.
|
|
7
|
-
import { LB_RESERVED_PREFIX } from "../core/lb-constants.js";
|
|
7
|
+
import { LB_RESERVED_PREFIX, QUERY_PARMS_ROW } from "../core/lb-constants.js";
|
|
8
8
|
/** A query that answers with one row. */
|
|
9
9
|
export function row(run) {
|
|
10
10
|
return { kind: "row", run };
|
|
@@ -31,6 +31,23 @@ export function list(run) {
|
|
|
31
31
|
export function patch(change) {
|
|
32
32
|
return { ...change };
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
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.
|
|
39
|
+
*
|
|
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
|
+
*
|
|
46
|
+
* Only parms. The path is never the server's to change.
|
|
47
|
+
*/
|
|
48
|
+
export function queryParms(parms) {
|
|
49
|
+
return { [QUERY_PARMS_ROW]: { ...parms } };
|
|
50
|
+
}
|
|
34
51
|
/**
|
|
35
52
|
* Build the engine over a set of pages. The pages are fixed at startup; the
|
|
36
53
|
* context is not, and arrives with each call.
|
|
@@ -78,14 +95,28 @@ export function createHub(pages) {
|
|
|
78
95
|
* set against the same context, laying what the operation itself stated
|
|
79
96
|
* over the refreshed queries — the narrower answer wins because it is the
|
|
80
97
|
* one that knows what actually changed.
|
|
98
|
+
*
|
|
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()`.
|
|
81
102
|
*/
|
|
82
103
|
async function settle(page, declared, where, ctx, notFound) {
|
|
83
104
|
if (!declared) {
|
|
84
105
|
console.warn(notFound);
|
|
85
106
|
return {};
|
|
86
107
|
}
|
|
87
|
-
const stated = await declared.run(ctx, where);
|
|
88
|
-
|
|
108
|
+
const { [QUERY_PARMS_ROW]: parms, ...stated } = (await declared.run(ctx, where)) ?? {};
|
|
109
|
+
if (parms !== undefined) {
|
|
110
|
+
const refused = refusal(parms);
|
|
111
|
+
if (refused === undefined) {
|
|
112
|
+
return { [QUERY_PARMS_ROW]: parms };
|
|
113
|
+
}
|
|
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`);
|
|
118
|
+
}
|
|
119
|
+
return { ...(await run(page, declared.refresh, ctx)), ...stated };
|
|
89
120
|
}
|
|
90
121
|
return {
|
|
91
122
|
async dataForPage(page, ctx) {
|
|
@@ -111,4 +142,23 @@ export function createHub(pages) {
|
|
|
111
142
|
},
|
|
112
143
|
};
|
|
113
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Why returned query parms cannot be used, or nothing when they can.
|
|
147
|
+
*
|
|
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.
|
|
151
|
+
*/
|
|
152
|
+
function refusal(parms) {
|
|
153
|
+
if (typeof parms !== "object" || parms === null || Array.isArray(parms)) {
|
|
154
|
+
return "that is not a row";
|
|
155
|
+
}
|
|
156
|
+
const values = Object.values(parms);
|
|
157
|
+
if (values.length === 0)
|
|
158
|
+
return "naming no parm";
|
|
159
|
+
if (!values.every((v) => typeof v === "string")) {
|
|
160
|
+
return `holding a value that is not a string; write queryParms({ name: "..." })`;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
114
164
|
//# 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 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, QUERY_PARMS_ROW } 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/**\n * Query parms a write decided, returned from a request's `run` when only the\n * write can know them: the key of a row it inserted, or the absence of one it\n * deleted. Each named parm is set, or taken out when its value is empty, and\n * every other parm is left as it is. Name at least one.\n *\n * The page then loads at that query string in the same round trip, as a\n * cold load of it would, so the refresh set is not run and whatever else\n * `run` returned is dropped: both were answers for the query string the page\n * is leaving. This is Post/Redirect/Get without the redirect. The hub writes the parms into the URL, replacing the history\n * entry, and lands the load.\n *\n * Only parms. The path is never the server's to change.\n */\nexport function queryParms(parms: Record<string, string>): HubData {\n return { [QUERY_PARMS_ROW]: { ...parms } };\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\n/**\n * The engine. Each of the four operations answers either with what its\n * refresh set and `run` produced, or, when `run` returned `queryParms()`,\n * with `QUERY_PARMS_ROW` alone. That second answer is not finished: the\n * caller builds a context from the query string with those parms set, calls\n * `dataForPage` with it, and sends the load with the row beside it.\n * `hubRoutes` does exactly that.\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 * An operation that changed the query parms answers with the parms alone.\n * Loading the page at them needs a context built from the new query\n * string, and building one is the caller's: see `queryParms()`.\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 { [QUERY_PARMS_ROW]: parms, ...stated } =\n (await declared.run(ctx, where as never)) ?? {};\n if (parms !== undefined) {\n const refused = refusal(parms);\n if (refused === undefined) {\n return { [QUERY_PARMS_ROW]: parms as Record<string, string> };\n }\n // The write has already happened, so the page still hears about it,\n // by its refresh set, as if no parms had been named.\n console.warn(\n `loadbare: page '${page}' returned ${QUERY_PARMS_ROW} ${refused}, ` +\n `ignoring it`,\n );\n }\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\n/**\n * Why returned query parms cannot be used, or nothing when they can.\n *\n * A value is a string, as a control's is and a URL's is. At least one parm\n * is named: a response naming none changes nothing, and reloading the whole\n * page at the same URL is not what this is for.\n */\nfunction refusal(parms: unknown): string | undefined {\n if (typeof parms !== \"object\" || parms === null || Array.isArray(parms)) {\n return \"that is not a row\";\n }\n const values = Object.values(parms);\n if (values.length === 0) return \"naming no parm\";\n if (!values.every((v) => typeof v === \"string\")) {\n return `holding a value that is not a string; write queryParms({ name: \"...\" })`;\n }\n return undefined;\n}\n"]}
|
package/docs/TECHREF-1.0.md
CHANGED
|
@@ -501,6 +501,9 @@ not a way to show a different detail per row.
|
|
|
501
501
|
|
|
502
502
|
Which master a page shows is a query parm, which a query reads off `ctx`
|
|
503
503
|
since it takes no argument from the browser — see [Query parms](#query-parms).
|
|
504
|
+
When a write creates or removes the master, the server response changes that
|
|
505
|
+
parm — see
|
|
506
|
+
[When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
|
|
504
507
|
|
|
505
508
|
### Requests
|
|
506
509
|
|
|
@@ -746,9 +749,54 @@ A control that writes a query parm sends no request. One that also carries
|
|
|
746
749
|
sent twice.
|
|
747
750
|
|
|
748
751
|
Every round trip carries the query string the browser is showing, a page
|
|
749
|
-
load and an action alike. The server
|
|
750
|
-
|
|
751
|
-
|
|
752
|
+
load and an action alike. The server hands its parms to `contextFor` — see
|
|
753
|
+
[The Express server](#the-express-server). Nothing in Loadbare assigns a
|
|
754
|
+
parm a meaning.
|
|
755
|
+
|
|
756
|
+
#### When a server response changes the query parms
|
|
757
|
+
|
|
758
|
+
Some query parms can only be known once a write has run. After an insert,
|
|
759
|
+
the key of the new row exists only on the server. After a delete, only the
|
|
760
|
+
server knows that the row the page was showing is gone.
|
|
761
|
+
|
|
762
|
+
The usual web answer is Post/Redirect/Get: the server answers the write with
|
|
763
|
+
a redirect to a URL naming the result, and the browser makes a second request
|
|
764
|
+
to load it. Loadbare/app does the same work in one round trip, and never
|
|
765
|
+
changes the path.
|
|
766
|
+
|
|
767
|
+
A request's `run` returns `queryParms()`, naming the parms the write decided.
|
|
768
|
+
The server loads the page with those parms set, as a cold load of the
|
|
769
|
+
resulting URL would: `onPageEnter`, then every query. The server response
|
|
770
|
+
carries the parms and that load together. The hub sets the parms in the URL,
|
|
771
|
+
replacing the history entry, and then lands the load.
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
rowInsert: {
|
|
775
|
+
run: async (ctx, { values }) => {
|
|
776
|
+
const id = await ctx.db.addAccount(values);
|
|
777
|
+
return queryParms({ acct: String(id) });
|
|
778
|
+
},
|
|
779
|
+
refresh: [],
|
|
780
|
+
},
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
- Only query parms change. A server response cannot send the browser to
|
|
784
|
+
another page.
|
|
785
|
+
- Parms the response does not name are left as they are. An empty value
|
|
786
|
+
removes its parm.
|
|
787
|
+
- At least one parm is named, and every value is a string. Otherwise the
|
|
788
|
+
server warns, ignores the parms, and runs the refresh set as usual.
|
|
789
|
+
- The refresh set does not run, and anything else `run` returned is dropped.
|
|
790
|
+
Both were answers for the query string the page is leaving.
|
|
791
|
+
- If loading the page fails, the write has still happened. The response
|
|
792
|
+
carries the parms alone, and the hub loads the page itself. A failure
|
|
793
|
+
there is a page load's, and is not stamped on the element that sent the
|
|
794
|
+
request.
|
|
795
|
+
- A user who has left the page by the time the response arrives keeps the
|
|
796
|
+
URL they are on.
|
|
797
|
+
|
|
798
|
+
The page is loaded with a second context, built by `contextFor` from the new
|
|
799
|
+
parms — see [The Express server](#the-express-server).
|
|
752
800
|
|
|
753
801
|
### lb-navigation
|
|
754
802
|
|
|
@@ -929,17 +977,23 @@ only real requirement is that the catch-all for app.html is at the end,
|
|
|
929
977
|
so it does not catch any other files.
|
|
930
978
|
|
|
931
979
|
`hubRoutes` answers `POST /lb/<page>`, with the query string the browser is
|
|
932
|
-
showing after it, verbatim.
|
|
933
|
-
`contextFor` puts on the context whatever a query
|
|
934
|
-
user input:
|
|
980
|
+
showing after it, verbatim. It hands `contextFor` that query string's parms
|
|
981
|
+
as a second argument, and `contextFor` puts on the context whatever a query
|
|
982
|
+
reads from them. They are user input:
|
|
935
983
|
|
|
936
984
|
```ts
|
|
937
|
-
function contextFor(req: Request): HubContext {
|
|
938
|
-
|
|
939
|
-
return { db: openDb(), acct: typeof acct === "string" ? acct : "" };
|
|
985
|
+
function contextFor(req: Request, parms: URLSearchParams): HubContext {
|
|
986
|
+
return { db: openDb(), acct: parms.get("acct") ?? "" };
|
|
940
987
|
}
|
|
941
988
|
```
|
|
942
989
|
|
|
990
|
+
Read the parms from that argument, not from `req.query`. After a request
|
|
991
|
+
whose `run` returned `queryParms()`, `contextFor` is called a second time for
|
|
992
|
+
the same request, with the new parms, to load the page at them. So it must
|
|
993
|
+
be safe to call twice, and a write must be visible to the second context by
|
|
994
|
+
the time its `run` returns. A handle opened per request without a
|
|
995
|
+
transaction around it is both.
|
|
996
|
+
|
|
943
997
|
Give the server the origin root. The hub reaches its own endpoints by
|
|
944
998
|
absolute path, so an application cannot be hosted under a subpath such as
|
|
945
999
|
`example.com/myapp/`, and anything proxying in front of the server passes
|
|
@@ -1010,6 +1064,13 @@ interaction happened, as `list`, `row`, `key`, `cell` and `value`. Under
|
|
|
1010
1064
|
ones. That is how a delta reaches the browser: wrap it in `patch()`, naming
|
|
1011
1065
|
the rows that arrived or changed and the keys that went.
|
|
1012
1066
|
|
|
1067
|
+
`run` may instead return `queryParms()`, naming query parms only the write
|
|
1068
|
+
can know, such as the key of a row it inserted. The page then loads at them
|
|
1069
|
+
in the same round trip, in place of the refresh set — see
|
|
1070
|
+
[When a server response changes the query parms](#when-a-server-response-changes-the-query-parms).
|
|
1071
|
+
A `rowDelete` that removes the row on screen returns `queryParms({ acct: "" })`
|
|
1072
|
+
and leaves any other delete to its refresh set.
|
|
1073
|
+
|
|
1013
1074
|
```ts
|
|
1014
1075
|
// members.requests.ts
|
|
1015
1076
|
import { patch, type Requests } from "@loadbare/app/server";
|
|
@@ -328,7 +328,12 @@ an absent parm lands empty, so the control shows what the address bar says.
|
|
|
328
328
|
A control that writes a query parm sends no request, and one that also
|
|
329
329
|
carries `lb-action` has that request refused.
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
A request can write parms too, when only the write knows their value, such as
|
|
332
|
+
the key of a row it inserted; see
|
|
333
|
+
[refresh and patch](./page-files.md#refresh-and-patch). The page loads at
|
|
334
|
+
them in the same round trip, and they land on their controls as above.
|
|
335
|
+
|
|
336
|
+
The server hands the parms to `contextFor`; see
|
|
332
337
|
[the Express server](./server.md#database-layer). See
|
|
333
338
|
[Query parms](../TECHREF-1.0.md#query-parms) for the whole rule.
|
|
334
339
|
|
|
@@ -192,3 +192,42 @@ resetRoster: {
|
|
|
192
192
|
refresh: ["roster"],
|
|
193
193
|
},
|
|
194
194
|
```
|
|
195
|
+
|
|
196
|
+
Return `queryParms()` instead when the server response changes the query
|
|
197
|
+
parms: after an insert, only the server knows the new key, and after a delete,
|
|
198
|
+
only the server knows the parm should go. The page loads at the query string
|
|
199
|
+
with those parms set, in the same round trip, and the hub writes them into the
|
|
200
|
+
URL, replacing the history entry. This is Post/Redirect/Get without the
|
|
201
|
+
redirect. The refresh set does not run, and nothing else `run` returned is
|
|
202
|
+
sent, since both answered for the query string the page left. Name at least
|
|
203
|
+
one parm, give each a string, and use an empty string to take one out:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// src/pages/accounts.requests.ts
|
|
207
|
+
import { queryParms, type Requests } from "@loadbare/app/server";
|
|
208
|
+
|
|
209
|
+
export const requests: Requests = {
|
|
210
|
+
crud: {
|
|
211
|
+
accounts: {
|
|
212
|
+
rowInsert: {
|
|
213
|
+
run: async (ctx, { values }) => {
|
|
214
|
+
const id = await ctx.db.addAccount(values);
|
|
215
|
+
return queryParms({ acct: String(id) });
|
|
216
|
+
},
|
|
217
|
+
refresh: [],
|
|
218
|
+
},
|
|
219
|
+
rowDelete: {
|
|
220
|
+
run: async (ctx, { key }) => {
|
|
221
|
+
await ctx.db.deleteAccount(key);
|
|
222
|
+
if (key === ctx.acct) return queryParms({ acct: "" });
|
|
223
|
+
},
|
|
224
|
+
refresh: ["accounts"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Only parms: `run` cannot send the browser to another page. The loaded page
|
|
232
|
+
reads the new parms through `contextFor` like any others; see
|
|
233
|
+
[the Express server](./server.md#database-layer).
|
package/docs/reference/server.md
CHANGED
|
@@ -137,17 +137,23 @@ declare module "@loadbare/app/server" {
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
The query string the browser is showing arrives on every data request,
|
|
141
|
-
`
|
|
142
|
-
reads from them, and treat them as user input:
|
|
140
|
+
The query string the browser is showing arrives on every data request, and
|
|
141
|
+
`contextFor` gets its parms as a second argument. Put on the context whatever
|
|
142
|
+
a query reads from them, and treat them as user input:
|
|
143
143
|
|
|
144
144
|
```ts
|
|
145
|
-
function contextFor(req: Request): HubContext {
|
|
146
|
-
|
|
147
|
-
return { db: openDb(), team: typeof team === "string" ? team : "" };
|
|
145
|
+
function contextFor(req: Request, parms: URLSearchParams): HubContext {
|
|
146
|
+
return { db: openDb(), team: parms.get("team") ?? "" };
|
|
148
147
|
}
|
|
149
148
|
```
|
|
150
149
|
|
|
150
|
+
Read the parms from that argument, not from `req.query`. When a server
|
|
151
|
+
response changes the query parms, `contextFor` is called a second time for
|
|
152
|
+
that request, with the new parms, to load the page at them; see [refresh and patch](./page-files.md#refresh-and-patch). Write it to
|
|
153
|
+
be safe to call twice, and make a write visible to the second context by the
|
|
154
|
+
time `run` returns: a handle opened per request, with no transaction held
|
|
155
|
+
open across the two, is both.
|
|
156
|
+
|
|
151
157
|
Add a field for anything else a request needs — the authenticated user, a
|
|
152
158
|
request id, a feature flag set. Queries and requests read them from `ctx`; see
|
|
153
159
|
[page files](./page-files.md).
|
package/docs/roadmap.md
CHANGED
|
@@ -34,6 +34,18 @@ A value that hasn't arrived yet is probably derivable from an absent
|
|
|
34
34
|
`lb-value` rather than needing a signal of its own. Not yet needed because
|
|
35
35
|
nothing currently produces that gap in practice — revisit if one does.
|
|
36
36
|
|
|
37
|
+
### Events while a request is in flight
|
|
38
|
+
|
|
39
|
+
A native action, a button or a form, ignores being performed again while its
|
|
40
|
+
own round trip is in flight. Nothing holds back any other element, so two
|
|
41
|
+
round trips can be out at once and the one that answers last is what lands,
|
|
42
|
+
whichever the user started last.
|
|
43
|
+
|
|
44
|
+
Whether the hub should refuse every event while anything is in flight is
|
|
45
|
+
open. A widget is deliberately not held back today, because one that sends on
|
|
46
|
+
change must have its latest value sent rather than dropped, and a refusal of
|
|
47
|
+
everything would have to say what happens to that value.
|
|
48
|
+
|
|
37
49
|
### Validation placement
|
|
38
50
|
|
|
39
51
|
Per-keystroke feedback cannot afford a round trip, so some validation will
|