@loadbare/app 0.25.0 → 0.26.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/build/assemble.d.ts +1 -2
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +11 -53
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/cli.d.ts +4 -10
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +11 -33
- package/dist/build/cli.js.map +1 -1
- package/dist/build/declared.js +1 -1
- package/dist/build/declared.js.map +1 -1
- package/dist/core/lb-constants.d.ts +8 -18
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +32 -72
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +12 -11
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +7 -0
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +10 -37
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +139 -522
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +115 -126
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/hub/lb-lifecycle.d.ts +2 -2
- package/dist/hub/lb-lifecycle.d.ts.map +1 -1
- package/dist/hub/lb-lifecycle.js +9 -17
- package/dist/hub/lb-lifecycle.js.map +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +3 -3
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +35 -19
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +141 -88
- package/dist/server/lb-server.js.map +1 -1
- package/docs/binding-cases.md +150 -0
- package/docs/comparison.md +18 -29
- package/docs/miswarts.md +2 -2
- package/docs/reference/css.md +2 -15
- package/docs/reference/custom-elements.md +6 -8
- package/docs/reference/data-binding.md +37 -180
- package/docs/reference/names.md +10 -26
- package/docs/reference/page-files.md +1 -12
- package/docs/reference/server.md +0 -1
- package/docs/testing.md +2 -5
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +52 -54
- package/skills/loadbare-app/references/css.md +2 -15
- package/skills/loadbare-app/references/custom-elements.md +6 -8
- package/skills/loadbare-app/references/data-binding.md +37 -180
- package/skills/loadbare-app/references/miswarts.md +2 -2
- package/skills/loadbare-app/references/names.md +10 -26
- package/skills/loadbare-app/references/page-files.md +1 -12
- package/skills/loadbare-app/references/server.md +0 -1
|
@@ -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/<stub>?<query string>\n * an empty body asks for the page's queries, or for\n * the page loaded at the new URL its onPageEnter\n * returned; a body carrying a request runs its\n * handler and answers with the refresh set, or with\n * the page loaded at the new URL the handler\n * returned\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 LB_ENDPOINT,\n LB_REFUSAL_STATUS,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\nimport {\n nextLocation,\n type HubRequest,\n type HubResponse,\n} from \"../core/lb-types.js\";\nimport { Refusal, type Hub, type 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\n/**\n * The router's options, one key per concern.\n *\n * `json` is handed to `express.json()` untouched, so it takes whatever that\n * takes. The one an application is likely to want is `limit`: the default\n * of 100kb refuses a page that sends a file's text as a value.\n */\nexport interface HubRoutesOptions {\n json?: Parameters<typeof express.json>[0];\n}\n\nexport function hubRoutes(\n hub: Hub,\n contextFor: ContextFor,\n options: HubRoutesOptions = {},\n): 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(options.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 queries. A body that carries something but no name is\n // malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n const items = await hub.dataForPage(page, ctx, { parms });\n const moved = items.find((item) => item.query === URL_QUERY);\n if (moved === undefined) {\n res.json(items);\n return;\n }\n // onPageEnter moved the URL, so the page loads there, and that load\n // does not follow onPageEnter again: no page can move the URL forever.\n res.json(\n await loadAt(\n moved,\n nextLocation(page, parms, moved.row ?? {}),\n `entering '${page}'`,\n ),\n );\n return;\n }\n\n if (!request.name) {\n console.warn(`loadbare: request without a name`);\n res.status(400).json([]);\n return;\n }\n\n // A thrown error is how application code (a query, a handler) says\n // \"this request is invalid\" or hits an unexpected failure — it resolves\n // to a real response rather than an unhandled rejection, so the browser\n // side has something to catch.\n // A refusal carries words the handler wrote for the user, and goes to\n // the browser with them. Anything else thrown is a failure, and goes\n // with nothing, so a stack or a database message never reaches a page.\n let items: HubResponse;\n try {\n items = await hub.runRequest(page, request, ctx);\n } catch (err) {\n if (err instanceof Refusal) {\n res.status(LB_REFUSAL_STATUS).json({ refusal: err.refusal });\n return;\n }\n console.error(`loadbare: '${request.name}' threw`, err);\n res.status(500).json([]);\n return;\n }\n\n const moved = items.find((item) => item.query === URL_QUERY);\n if (moved === undefined) {\n res.json(items);\n return;\n }\n // The handler returned a new row for lb-url, so the page loads there, as\n // a cold load of the new URL would. The handler has already run: from\n // here a failure must not read as the request failing, or the user would\n // send it again. The item goes back alone instead, and the hub, writing\n // the URL with nothing to land, loads the page itself.\n res.json(\n await loadAt(\n moved,\n nextLocation(page, parms, moved.row ?? {}),\n `'${request.name}'`,\n ),\n );\n\n /**\n * The `lb-url` item and the page loaded where it leads. A load that\n * throws is not the failure of what moved the URL: the item goes back\n * alone, and the hub, writing the URL with nothing to land, loads the\n * page itself.\n */\n async function loadAt(\n moved: HubResponse[number],\n next: { page: string; parms: URLSearchParams },\n what: string,\n ): Promise<HubResponse> {\n try {\n const load = await hub.dataForPage(\n next.page,\n contextFor(req, next.parms),\n { follow: false },\n );\n return [moved, ...load];\n } catch (err) {\n console.error(\n `loadbare: ${what} moved the URL, and loading '${next.page}' ` +\n `there threw`,\n err,\n );\n return [moved];\n }\n }\n });\n\n // A body that never parsed, or was too large to read, is refused like any\n // other malformed request, rather than falling through to the\n // application's error handler, which would print a stack and call it a\n // server failure. Only for the hub's own path: express.json() above sees\n // every request, and a bad body sent to one of the application's routes is\n // the application's to answer.\n router.use(\n `${LB_ENDPOINT}/*page`,\n (err: unknown, _req: Request, res: Response, next: NextFunction) => {\n const { type, limit } = err as { type?: string; limit?: number };\n if (type === \"entity.parse.failed\") {\n console.warn(`loadbare: request body is not JSON`);\n res.status(400).json([]);\n return;\n }\n if (type === \"entity.too.large\") {\n // Named in the log, since nothing in the browser says which limit\n // was hit or where to raise it.\n console.warn(\n `loadbare: request body is over the limit of ${limit} bytes; ` +\n `raise it with hubRoutes' json.limit option`,\n );\n res.status(413).json([]);\n return;\n }\n next(err);\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/<stub>?<query string>\n * an empty body asks for the page's queries, or for\n * the page loaded at the new URL its onPageEnter\n * returned; a body carrying a request runs its\n * handler and answers with the refresh set, or with\n * the page loaded at the new URL the handler\n * returned\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 LB_ENDPOINT,\n LB_REFUSAL_STATUS,\n URL_QUERY,\n} from \"../core/lb-constants.js\";\nimport {\n nextLocation,\n oneRow,\n type HubRequest,\n type HubResponse,\n} from \"../core/lb-types.js\";\nimport { Refusal, type Hub, type 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\n/**\n * The router's options, one key per concern.\n *\n * `json` is handed to `express.json()` untouched, so it takes whatever that\n * takes. The one an application is likely to want is `limit`: the default\n * of 100kb refuses a page that sends a file's text as a value.\n */\nexport interface HubRoutesOptions {\n json?: Parameters<typeof express.json>[0];\n}\n\nexport function hubRoutes(\n hub: Hub,\n contextFor: ContextFor,\n options: HubRoutesOptions = {},\n): 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(options.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 queries. A body that carries something but no name is\n // malformed.\n if (request === undefined || Object.keys(request).length === 0) {\n const items = await hub.dataForPage(page, ctx, { parms });\n const moved = items.find((item) => item.query === URL_QUERY);\n if (moved === undefined) {\n res.json(items);\n return;\n }\n // onPageEnter moved the URL, so the page loads there, and that load\n // does not follow onPageEnter again: no page can move the URL forever.\n res.json(\n await loadAt(\n moved,\n nextLocation(page, parms, oneRow(moved) ?? {}),\n `entering '${page}'`,\n ),\n );\n return;\n }\n\n if (!request.name) {\n console.warn(`loadbare: request without a name`);\n res.status(400).json([]);\n return;\n }\n\n // A thrown error is how application code (a query, a handler) says\n // \"this request is invalid\" or hits an unexpected failure — it resolves\n // to a real response rather than an unhandled rejection, so the browser\n // side has something to catch.\n // A refusal carries words the handler wrote for the user, and goes to\n // the browser with them. Anything else thrown is a failure, and goes\n // with nothing, so a stack or a database message never reaches a page.\n let items: HubResponse;\n try {\n items = await hub.runRequest(page, request, ctx);\n } catch (err) {\n if (err instanceof Refusal) {\n res.status(LB_REFUSAL_STATUS).json({ refusal: err.refusal });\n return;\n }\n console.error(`loadbare: '${request.name}' threw`, err);\n res.status(500).json([]);\n return;\n }\n\n const moved = items.find((item) => item.query === URL_QUERY);\n if (moved === undefined) {\n res.json(items);\n return;\n }\n // The handler returned a new row for lb-url, so the page loads there, as\n // a cold load of the new URL would. The handler has already run: from\n // here a failure must not read as the request failing, or the user would\n // send it again. The item goes back alone instead, and the hub, writing\n // the URL with nothing to land, loads the page itself.\n res.json(\n await loadAt(\n moved,\n nextLocation(page, parms, oneRow(moved) ?? {}),\n `'${request.name}'`,\n ),\n );\n\n /**\n * The `lb-url` item and the page loaded where it leads. A load that\n * throws is not the failure of what moved the URL: the item goes back\n * alone, and the hub, writing the URL with nothing to land, loads the\n * page itself.\n */\n async function loadAt(\n moved: HubResponse[number],\n next: { page: string; parms: URLSearchParams },\n what: string,\n ): Promise<HubResponse> {\n try {\n const load = await hub.dataForPage(\n next.page,\n contextFor(req, next.parms),\n { follow: false },\n );\n return [moved, ...load];\n } catch (err) {\n console.error(\n `loadbare: ${what} moved the URL, and loading '${next.page}' ` +\n `there threw`,\n err,\n );\n return [moved];\n }\n }\n });\n\n // A body that never parsed, or was too large to read, is refused like any\n // other malformed request, rather than falling through to the\n // application's error handler, which would print a stack and call it a\n // server failure. Only for the hub's own path: express.json() above sees\n // every request, and a bad body sent to one of the application's routes is\n // the application's to answer.\n router.use(\n `${LB_ENDPOINT}/*page`,\n (err: unknown, _req: Request, res: Response, next: NextFunction) => {\n const { type, limit } = err as { type?: string; limit?: number };\n if (type === \"entity.parse.failed\") {\n console.warn(`loadbare: request body is not JSON`);\n res.status(400).json([]);\n return;\n }\n if (type === \"entity.too.large\") {\n // Named in the log, since nothing in the browser says which limit\n // was hit or where to raise it.\n console.warn(\n `loadbare: request body is over the limit of ${limit} bytes; ` +\n `raise it with hubRoutes' json.limit option`,\n );\n res.status(413).json([]);\n return;\n }\n next(err);\n },\n );\n\n return router;\n}\n"]}
|
|
@@ -20,6 +20,10 @@ import type { ColumnTypes, HubData, HubRequest, HubResponse, HubResult, Kind, Ga
|
|
|
20
20
|
*/
|
|
21
21
|
export interface HubContext {
|
|
22
22
|
}
|
|
23
|
+
/** What answers a `row` query. */
|
|
24
|
+
type RowRun = (ctx: HubContext) => HubResult | Promise<HubResult>;
|
|
25
|
+
/** What answers a `rows` query, given its resolved order. */
|
|
26
|
+
type RowsRun = (ctx: HubContext, order: string) => HubResult | Promise<HubResult>;
|
|
23
27
|
/**
|
|
24
28
|
* A declared query: its kind, its key, and the run that answers it.
|
|
25
29
|
*
|
|
@@ -33,7 +37,7 @@ export interface HubContext {
|
|
|
33
37
|
export interface Query {
|
|
34
38
|
readonly kind: Kind;
|
|
35
39
|
readonly key: string;
|
|
36
|
-
readonly run:
|
|
40
|
+
readonly run: RowRun | RowsRun;
|
|
37
41
|
/** A `rows` query's order, as `rows()` declared it. */
|
|
38
42
|
readonly order?: string;
|
|
39
43
|
/** A `rows` query's column types, as `rows()` declared them. */
|
|
@@ -44,12 +48,12 @@ export interface Query {
|
|
|
44
48
|
*
|
|
45
49
|
* `order` is the order the hub places its rows by: columns separated by
|
|
46
50
|
* commas, each descending when it begins with `-`, as in
|
|
47
|
-
* `category_order,-opened`. An order the user chose, held in `lb-
|
|
51
|
+
* `category_order,-opened`. An order the user chose, held in `lb-sort`,
|
|
48
52
|
* replaces it. With neither, rows show in the order the query answered them
|
|
49
53
|
* in.
|
|
50
54
|
*
|
|
51
|
-
* `types` names a type for any of its columns, and the hub sorts
|
|
52
|
-
*
|
|
55
|
+
* `types` names a type for any of its columns, and the hub sorts and
|
|
56
|
+
* filters that column read as its type rather
|
|
53
57
|
* than by the JSON type its values arrive as: `{ amount: "number" }` for a
|
|
54
58
|
* Postgres `numeric` that arrives as a string. A column it does not name
|
|
55
59
|
* compares by its JSON type. The order and the types go to the browser
|
|
@@ -68,8 +72,12 @@ export declare function row(key: string, run: (ctx: HubContext) => Row | null |
|
|
|
68
72
|
* A query that answers with all its rows. Rows land by their `key` column: a
|
|
69
73
|
* row whose key is not in the answer is gone. The hub places them by the
|
|
70
74
|
* order in `options`, or in the order they arrived when there is none.
|
|
75
|
+
*
|
|
76
|
+
* `run` receives the resolved order — the user's, from `lb-sort`, or the
|
|
77
|
+
* one `options` declares — and orders its SQL by it, so the rows it answers
|
|
78
|
+
* arrive in the order the page shows them in.
|
|
71
79
|
*/
|
|
72
|
-
export declare function rows(key: string, run: (ctx: HubContext) => Row[] | Promise<Row[]>, options?: RowsOptions): Query;
|
|
80
|
+
export declare function rows(key: string, run: (ctx: HubContext, order: string) => Row[] | Promise<Row[]>, options?: RowsOptions): Query;
|
|
73
81
|
/**
|
|
74
82
|
* Only what changed, returned from a handler for a `rows` query. Rows named
|
|
75
83
|
* here are added or updated, keys in `drop` are removed, and everything
|
|
@@ -99,14 +107,17 @@ export declare function patch(change: {
|
|
|
99
107
|
*/
|
|
100
108
|
export declare function url(columns: Record<string, string | string[]>): HubData;
|
|
101
109
|
/**
|
|
102
|
-
* The
|
|
103
|
-
* what the user last chose
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
* The resolved order for each `rows` query, returned from `onPageEnter` to
|
|
111
|
+
* restore what the user last chose. Each key is a query name and its value
|
|
112
|
+
* the order the query runs its rows in, as `rows()` declares one.
|
|
113
|
+
*/
|
|
114
|
+
export declare function sort(orders: Record<string, string>): HubData;
|
|
115
|
+
/**
|
|
116
|
+
* The user's filters for the page, returned from `onPageEnter` to restore
|
|
117
|
+
* them. Each key is a query name and its value the filter values for that
|
|
118
|
+
* query's rows, by column.
|
|
108
119
|
*/
|
|
109
|
-
export declare function
|
|
120
|
+
export declare function filters(rows: Record<string, Record<string, string>>): HubData;
|
|
110
121
|
/**
|
|
111
122
|
* A handler's refusal: thrown, and answered to the browser with the words
|
|
112
123
|
* it carries. Every other error a handler throws reaches the browser as a
|
|
@@ -185,12 +196,12 @@ export interface Crud {
|
|
|
185
196
|
* page to other query parms or another page before anything shows, as an
|
|
186
197
|
* application restoring what the user last had on screen does. The page
|
|
187
198
|
* then loads there in the same round trip, and `onPageEnter` running for
|
|
188
|
-
* that load is not followed again. It may return `
|
|
189
|
-
* or alone, to restore the user's choices for the page.
|
|
199
|
+
* that load is not followed again. It may return `sort()`, `filters()`,
|
|
200
|
+
* with `url()` or alone, to restore the user's choices for the page.
|
|
190
201
|
*
|
|
191
|
-
* `onChoices` runs each time the user
|
|
192
|
-
*
|
|
193
|
-
* restore. It answers with nothing, by convention.
|
|
202
|
+
* `onChoices` runs each time the user changes the sort or a filter, with
|
|
203
|
+
* the query and its new sort or filters, so an application can keep them
|
|
204
|
+
* for `onPageEnter` to restore. It answers with nothing, by convention.
|
|
194
205
|
*
|
|
195
206
|
* `handlers` holds the page's declared requests, by name. A name the page
|
|
196
207
|
* did not declare is refused, so the wire cannot reach anything the page has
|
|
@@ -200,7 +211,11 @@ export interface Crud {
|
|
|
200
211
|
*/
|
|
201
212
|
export interface Requests {
|
|
202
213
|
onPageEnter?: (ctx: HubContext) => void | HubData | Promise<void | HubData>;
|
|
203
|
-
onChoices?: (ctx: HubContext,
|
|
214
|
+
onChoices?: (ctx: HubContext, change: {
|
|
215
|
+
query: string;
|
|
216
|
+
sort?: string;
|
|
217
|
+
filters?: Record<string, string>;
|
|
218
|
+
}) => void | HubData | Promise<void | HubData>;
|
|
204
219
|
handlers?: Record<string, Handler>;
|
|
205
220
|
crud?: Record<string, Crud>;
|
|
206
221
|
}
|
|
@@ -211,7 +226,7 @@ export interface Page {
|
|
|
211
226
|
}
|
|
212
227
|
/**
|
|
213
228
|
* The page registry. The builder writes it from every `.requests.ts` and
|
|
214
|
-
* `.queries.ts` it discovers
|
|
229
|
+
* `.queries.ts` it discovers.
|
|
215
230
|
*/
|
|
216
231
|
export type Pages = Record<string, Page>;
|
|
217
232
|
/**
|
|
@@ -244,4 +259,5 @@ export interface Hub {
|
|
|
244
259
|
* context is not, and arrives with each call.
|
|
245
260
|
*/
|
|
246
261
|
export declare function createHub(pages: Pages): Hub;
|
|
262
|
+
export {};
|
|
247
263
|
//# sourceMappingURL=lb-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,WAAW,EACX,OAAO,EACP,UAAU,EACV,WAAW,EACX,SAAS,EACT,IAAI,EACJ,WAAW,EACX,UAAU,EACV,KAAK,EACL,aAAa,EAEb,GAAG,EACJ,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
|
package/dist/server/lb-server.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// vocabulary the browser writes, this one is the vocabulary the application
|
|
5
5
|
// writes. The engine below is the whole of Loadbare on the server. It ships
|
|
6
6
|
// no HTTP server, no router, and no data layer.
|
|
7
|
-
import { isRowRequest, nextLocation, parseOrder } from "../core/lb-types.js";
|
|
8
|
-
import {
|
|
7
|
+
import { isRowRequest, nextLocation, oneRow, parseOrder } from "../core/lb-types.js";
|
|
8
|
+
import { COLUMN_TYPES, FILTERS_COLUMN_QUERY, FILTERS_QUERY, KIND_ROW, KIND_ROWS, LB_RESERVED_PREFIX, REQUEST_READ, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, SORT_COLUMN_ORDER, SORT_COLUMN_QUERY, SORT_QUERY, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
|
|
9
9
|
/**
|
|
10
10
|
* A query that answers with one row, identified by its `key` column, or with
|
|
11
11
|
* `null` when there is none.
|
|
@@ -17,6 +17,10 @@ export function row(key, run) {
|
|
|
17
17
|
* A query that answers with all its rows. Rows land by their `key` column: a
|
|
18
18
|
* row whose key is not in the answer is gone. The hub places them by the
|
|
19
19
|
* order in `options`, or in the order they arrived when there is none.
|
|
20
|
+
*
|
|
21
|
+
* `run` receives the resolved order — the user's, from `lb-sort`, or the
|
|
22
|
+
* one `options` declares — and orders its SQL by it, so the rows it answers
|
|
23
|
+
* arrive in the order the page shows them in.
|
|
20
24
|
*/
|
|
21
25
|
export function rows(key, run, options = {}) {
|
|
22
26
|
return {
|
|
@@ -64,15 +68,30 @@ export function url(columns) {
|
|
|
64
68
|
return { [URL_QUERY]: { ...columns } };
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
67
|
-
* The
|
|
68
|
-
* what the user last chose
|
|
69
|
-
*
|
|
70
|
-
* Return it beside `url()` with the spread both are written for:
|
|
71
|
-
*
|
|
72
|
-
* return { ...url({ acct: "23" }), ...choices({ sort: "-amount" }) };
|
|
71
|
+
* The resolved order for each `rows` query, returned from `onPageEnter` to
|
|
72
|
+
* restore what the user last chose. Each key is a query name and its value
|
|
73
|
+
* the order the query runs its rows in, as `rows()` declares one.
|
|
73
74
|
*/
|
|
74
|
-
export function
|
|
75
|
-
return {
|
|
75
|
+
export function sort(orders) {
|
|
76
|
+
return {
|
|
77
|
+
[SORT_QUERY]: Object.entries(orders).map(([query, order]) => ({
|
|
78
|
+
[SORT_COLUMN_QUERY]: query,
|
|
79
|
+
[SORT_COLUMN_ORDER]: order,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The user's filters for the page, returned from `onPageEnter` to restore
|
|
85
|
+
* them. Each key is a query name and its value the filter values for that
|
|
86
|
+
* query's rows, by column.
|
|
87
|
+
*/
|
|
88
|
+
export function filters(rows) {
|
|
89
|
+
return {
|
|
90
|
+
[FILTERS_QUERY]: Object.entries(rows).map(([query, values]) => ({
|
|
91
|
+
[FILTERS_COLUMN_QUERY]: query,
|
|
92
|
+
...values,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
76
95
|
}
|
|
77
96
|
/**
|
|
78
97
|
* A handler's refusal: thrown, and answered to the browser with the words
|
|
@@ -197,9 +216,8 @@ export function createHub(pages) {
|
|
|
197
216
|
`${Array.isArray(result) ? "rows" : "a patch"}`);
|
|
198
217
|
continue;
|
|
199
218
|
}
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
item.row = (result ?? null);
|
|
219
|
+
// Every answer is rows: a `row` query's is its row, or none.
|
|
220
|
+
item.rows = result == null ? [] : [result];
|
|
203
221
|
}
|
|
204
222
|
else {
|
|
205
223
|
if (result === null || result === undefined) {
|
|
@@ -210,8 +228,6 @@ export function createHub(pages) {
|
|
|
210
228
|
item.rows = result;
|
|
211
229
|
else
|
|
212
230
|
item.patch = result;
|
|
213
|
-
if (load && query.order !== undefined)
|
|
214
|
-
item.order = query.order;
|
|
215
231
|
if (load && query.types !== undefined)
|
|
216
232
|
item.types = query.types;
|
|
217
233
|
}
|
|
@@ -219,24 +235,35 @@ export function createHub(pages) {
|
|
|
219
235
|
}
|
|
220
236
|
return out;
|
|
221
237
|
}
|
|
222
|
-
/**
|
|
223
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Run queries by name, each answering with all its rows or its row. A
|
|
240
|
+
* `rows` query receives the resolved order for its name from `sorts`, or
|
|
241
|
+
* its declared default, and the order each ran with is returned beside the
|
|
242
|
+
* data for the caller to write to `lb-sort`.
|
|
243
|
+
*/
|
|
244
|
+
async function run(page, names, ctx, sorts) {
|
|
224
245
|
const data = {};
|
|
246
|
+
const orders = new Map();
|
|
225
247
|
for (const name of names) {
|
|
226
248
|
const query = pages[page]?.queries[name];
|
|
227
249
|
if (!query) {
|
|
228
250
|
console.warn(`loadbare: page '${page}' declares no query '${name}'`);
|
|
229
251
|
continue;
|
|
230
252
|
}
|
|
231
|
-
const
|
|
253
|
+
const order = sorts?.get(name) ?? query.order ?? "";
|
|
254
|
+
const result = query.kind === KIND_ROWS
|
|
255
|
+
? await query.run(ctx, order)
|
|
256
|
+
: await query.run(ctx);
|
|
232
257
|
if (Array.isArray(result) !== (query.kind === KIND_ROWS)) {
|
|
233
258
|
console.warn(`loadbare: query '${name}' is declared ${query.kind} but answered ` +
|
|
234
259
|
`with ${Array.isArray(result) ? "rows" : result == null ? "nothing" : "one row"}`);
|
|
235
260
|
continue;
|
|
236
261
|
}
|
|
237
262
|
data[name] = result;
|
|
263
|
+
if (query.kind === KIND_ROWS && order !== "")
|
|
264
|
+
orders.set(name, order);
|
|
238
265
|
}
|
|
239
|
-
return data;
|
|
266
|
+
return { data, orders };
|
|
240
267
|
}
|
|
241
268
|
/**
|
|
242
269
|
* Run a handler, then its refresh set against the same context, laying
|
|
@@ -257,42 +284,7 @@ export function createHub(pages) {
|
|
|
257
284
|
if (item)
|
|
258
285
|
return [item];
|
|
259
286
|
return items(page, {
|
|
260
|
-
...(await run(page, handler.refresh, ctx)),
|
|
261
|
-
...stated,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* An update or a delete for several rows: the page's `crud` entry runs once
|
|
266
|
-
* per key, as it would for each row alone, so an application writes
|
|
267
|
-
* nothing more to allow it. What each run returns is laid together, a
|
|
268
|
-
* patch onto a patch, and the refresh set runs once.
|
|
269
|
-
*
|
|
270
|
-
* A run that returns `url()` is reported and its row ignored: a URL names
|
|
271
|
-
* one view, and several rows cannot each choose it.
|
|
272
|
-
*/
|
|
273
|
-
async function eachKey(page, name, fields, ctx) {
|
|
274
|
-
const { keys, ...one } = fields;
|
|
275
|
-
const query = fields.query ?? "";
|
|
276
|
-
const handler = pages[page]?.requests.crud?.[query]?.[CRUD_KEYS[name]];
|
|
277
|
-
if (!handler) {
|
|
278
|
-
console.warn(`loadbare: page '${page}' declares no ${CRUD_KEYS[name]} for '${query}'`);
|
|
279
|
-
return [];
|
|
280
|
-
}
|
|
281
|
-
if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string")) {
|
|
282
|
-
console.warn(`loadbare: '${name}' carries keys that are not strings`);
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
let stated = {};
|
|
286
|
-
for (const key of keys) {
|
|
287
|
-
const { [URL_QUERY]: moved, ...answer } = (await handler.run(ctx, { ...one, key })) ?? {};
|
|
288
|
-
if (moved !== undefined) {
|
|
289
|
-
console.warn(`loadbare: ${CRUD_KEYS[name]} for '${query}' returned url() for ` +
|
|
290
|
-
`one of several rows, ignoring it`);
|
|
291
|
-
}
|
|
292
|
-
stated = layered(stated, answer);
|
|
293
|
-
}
|
|
294
|
-
return items(page, {
|
|
295
|
-
...(await run(page, handler.refresh, ctx)),
|
|
287
|
+
...(await run(page, handler.refresh, ctx)).data,
|
|
296
288
|
...stated,
|
|
297
289
|
});
|
|
298
290
|
}
|
|
@@ -303,22 +295,32 @@ export function createHub(pages) {
|
|
|
303
295
|
console.warn(`loadbare: no page '${page}'`);
|
|
304
296
|
return [];
|
|
305
297
|
}
|
|
306
|
-
const { [URL_QUERY]: moved, [
|
|
298
|
+
const { [URL_QUERY]: moved, [SORT_QUERY]: sorts, [FILTERS_QUERY]: filters, ...stated } = (await entry.requests.onPageEnter?.(ctx)) ?? {};
|
|
307
299
|
if (Object.keys(stated).length > 0) {
|
|
308
300
|
console.warn(`loadbare: page '${page}' onPageEnter returned answers; it returns ` +
|
|
309
|
-
`url(),
|
|
301
|
+
`url(), sort(), filters() or nothing, ignoring them`);
|
|
310
302
|
}
|
|
311
303
|
const item = follow ? urlItem(page, moved) : undefined;
|
|
312
|
-
if (item && !(parms && leadsHere(page, parms, item
|
|
304
|
+
if (item && !(parms && leadsHere(page, parms, oneRow(item))))
|
|
313
305
|
return [item];
|
|
314
|
-
|
|
306
|
+
const { data, orders } = await run(page, Object.keys(entry.queries), ctx, sortsToMap(sorts));
|
|
307
|
+
// The sort and filters go first, so the rows that read them land by
|
|
308
|
+
// them.
|
|
315
309
|
return [
|
|
316
|
-
...
|
|
317
|
-
...
|
|
310
|
+
...sortItem(orders),
|
|
311
|
+
...filtersItem(page, filters),
|
|
312
|
+
...items(page, data, true),
|
|
318
313
|
];
|
|
319
314
|
},
|
|
320
|
-
runRequest(page, request, ctx) {
|
|
315
|
+
async runRequest(page, request, ctx) {
|
|
321
316
|
const { name, ...sent } = request;
|
|
317
|
+
// The hub's read: the queries named, at the parms the request was sent
|
|
318
|
+
// at, answered as any query is.
|
|
319
|
+
if (name === REQUEST_READ) {
|
|
320
|
+
const declared = pages[page]?.queries ?? {};
|
|
321
|
+
const names = (Array.isArray(sent.queries) ? sent.queries : []).filter((q) => typeof q === "string" && q in declared);
|
|
322
|
+
return run(page, names, ctx).then(({ data }) => items(page, data));
|
|
323
|
+
}
|
|
322
324
|
// Only a column of lb-url holds several values, and the hub answers
|
|
323
325
|
// those itself: a column the server receives holds one.
|
|
324
326
|
if (sent.values !== undefined && !stringValues(sent.values)) {
|
|
@@ -332,21 +334,26 @@ export function createHub(pages) {
|
|
|
332
334
|
`values by query, ignoring it`);
|
|
333
335
|
return Promise.resolve([]);
|
|
334
336
|
}
|
|
335
|
-
//
|
|
336
|
-
// not keeping them, and is not told.
|
|
337
|
-
if (fields.query ===
|
|
337
|
+
// A sort or filter the user changed, told to the page. One with no
|
|
338
|
+
// `onChoices` is not keeping them, and is not told.
|
|
339
|
+
if (fields.query === SORT_QUERY || fields.query === FILTERS_QUERY) {
|
|
338
340
|
const onChoices = pages[page]?.requests.onChoices;
|
|
339
341
|
if (name !== REQUEST_ROW_UPDATE || !onChoices) {
|
|
340
342
|
return Promise.resolve([]);
|
|
341
343
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
const query = fields.key ?? "";
|
|
345
|
+
const sort = fields.query === SORT_QUERY
|
|
346
|
+
? fields.values?.[SORT_COLUMN_ORDER]
|
|
347
|
+
: undefined;
|
|
348
|
+
const filters = fields.query === FILTERS_QUERY ? fields.values : undefined;
|
|
349
|
+
await onChoices(ctx, {
|
|
350
|
+
query,
|
|
351
|
+
...(sort !== undefined && { sort }),
|
|
352
|
+
...(filters !== undefined && { filters }),
|
|
353
|
+
});
|
|
354
|
+
// The browser re-sorts or re-filters the rows it holds; the server
|
|
355
|
+
// only persists the change for the next page.
|
|
356
|
+
return [];
|
|
350
357
|
}
|
|
351
358
|
if (isRowRequest(name)) {
|
|
352
359
|
const query = fields.query ?? "";
|
|
@@ -413,31 +420,78 @@ function urlItem(page, moved) {
|
|
|
413
420
|
query: URL_QUERY,
|
|
414
421
|
kind: KIND_ROW,
|
|
415
422
|
key: URL_COLUMN_PATH,
|
|
416
|
-
|
|
423
|
+
rows: [moved],
|
|
417
424
|
};
|
|
418
425
|
}
|
|
419
426
|
console.warn(`loadbare: page '${page}' returned ${URL_QUERY} ${refused}, ignoring it`);
|
|
420
427
|
return undefined;
|
|
421
428
|
}
|
|
429
|
+
/** The sorts `onPageEnter` returned, as a map of query name to order. */
|
|
430
|
+
function sortsToMap(sorts) {
|
|
431
|
+
const map = new Map();
|
|
432
|
+
if (!Array.isArray(sorts))
|
|
433
|
+
return map;
|
|
434
|
+
for (const row of sorts) {
|
|
435
|
+
if (typeof row !== "object" || row === null)
|
|
436
|
+
continue;
|
|
437
|
+
const r = row;
|
|
438
|
+
if (typeof r[SORT_COLUMN_QUERY] === "string" &&
|
|
439
|
+
typeof r[SORT_COLUMN_ORDER] === "string") {
|
|
440
|
+
map.set(r[SORT_COLUMN_QUERY], r[SORT_COLUMN_ORDER]);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return map;
|
|
444
|
+
}
|
|
422
445
|
/**
|
|
423
|
-
* The `lb-
|
|
424
|
-
* when
|
|
446
|
+
* The `lb-sort` item for the orders each `rows` query ran with, or nothing
|
|
447
|
+
* when none ran with an order.
|
|
425
448
|
*/
|
|
426
|
-
function
|
|
427
|
-
if (
|
|
449
|
+
function sortItem(orders) {
|
|
450
|
+
if (orders.size === 0)
|
|
428
451
|
return [];
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
452
|
+
return [
|
|
453
|
+
{
|
|
454
|
+
query: SORT_QUERY,
|
|
455
|
+
kind: KIND_ROWS,
|
|
456
|
+
key: SORT_COLUMN_QUERY,
|
|
457
|
+
rows: [...orders.entries()].map(([query, order]) => ({
|
|
458
|
+
[SORT_COLUMN_QUERY]: query,
|
|
459
|
+
[SORT_COLUMN_ORDER]: order,
|
|
460
|
+
})),
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The `lb-filters` item for the filters `onPageEnter` returned, or nothing
|
|
466
|
+
* when it returned none. Each row names its query and its filter values, by
|
|
467
|
+
* column.
|
|
468
|
+
*/
|
|
469
|
+
function filtersItem(page, filters) {
|
|
470
|
+
if (filters === undefined)
|
|
433
471
|
return [];
|
|
472
|
+
if (!Array.isArray(filters)) {
|
|
473
|
+
console.warn(`loadbare: page '${page}' returned ${FILTERS_QUERY} that is not a ` +
|
|
474
|
+
`list, ignoring it`);
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const rows = [];
|
|
478
|
+
for (const row of filters) {
|
|
479
|
+
if (typeof row !== "object" || row === null)
|
|
480
|
+
continue;
|
|
481
|
+
const r = row;
|
|
482
|
+
if (typeof r[FILTERS_COLUMN_QUERY] !== "string")
|
|
483
|
+
continue;
|
|
484
|
+
const values = Object.fromEntries(Object.entries(r).filter(([key, value]) => key !== FILTERS_COLUMN_QUERY && typeof value === "string"));
|
|
485
|
+
rows.push({ [FILTERS_COLUMN_QUERY]: r[FILTERS_COLUMN_QUERY], ...values });
|
|
434
486
|
}
|
|
487
|
+
if (rows.length === 0)
|
|
488
|
+
return [];
|
|
435
489
|
return [
|
|
436
490
|
{
|
|
437
|
-
query:
|
|
438
|
-
kind:
|
|
439
|
-
key:
|
|
440
|
-
|
|
491
|
+
query: FILTERS_QUERY,
|
|
492
|
+
kind: KIND_ROWS,
|
|
493
|
+
key: FILTERS_COLUMN_QUERY,
|
|
494
|
+
rows,
|
|
441
495
|
},
|
|
442
496
|
];
|
|
443
497
|
}
|
|
@@ -447,8 +501,7 @@ function leadsHere(page, parms, row) {
|
|
|
447
501
|
return next.page === page && next.parms.toString() === parms.toString();
|
|
448
502
|
}
|
|
449
503
|
/**
|
|
450
|
-
* Why a returned row for `lb-url`
|
|
451
|
-
* when it can.
|
|
504
|
+
* Why a returned row for `lb-url` cannot be used, or nothing when it can.
|
|
452
505
|
*
|
|
453
506
|
* A value is a string, as a control's is and a URL's is, or for `lb-url`,
|
|
454
507
|
* whose parms a URL may repeat, an array of strings. At least one column is
|