@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.
Files changed (55) hide show
  1. package/dist/build/assemble.d.ts +1 -2
  2. package/dist/build/assemble.d.ts.map +1 -1
  3. package/dist/build/assemble.js +11 -53
  4. package/dist/build/assemble.js.map +1 -1
  5. package/dist/build/cli.d.ts +4 -10
  6. package/dist/build/cli.d.ts.map +1 -1
  7. package/dist/build/cli.js +11 -33
  8. package/dist/build/cli.js.map +1 -1
  9. package/dist/build/declared.js +1 -1
  10. package/dist/build/declared.js.map +1 -1
  11. package/dist/core/lb-constants.d.ts +8 -18
  12. package/dist/core/lb-constants.d.ts.map +1 -1
  13. package/dist/core/lb-constants.js +32 -72
  14. package/dist/core/lb-constants.js.map +1 -1
  15. package/dist/core/lb-types.d.ts +12 -11
  16. package/dist/core/lb-types.d.ts.map +1 -1
  17. package/dist/core/lb-types.js +7 -0
  18. package/dist/core/lb-types.js.map +1 -1
  19. package/dist/hub/lb-apply.d.ts +10 -37
  20. package/dist/hub/lb-apply.d.ts.map +1 -1
  21. package/dist/hub/lb-apply.js +139 -522
  22. package/dist/hub/lb-apply.js.map +1 -1
  23. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  24. package/dist/hub/lb-hub.browser.js +115 -126
  25. package/dist/hub/lb-hub.browser.js.map +1 -1
  26. package/dist/hub/lb-lifecycle.d.ts +2 -2
  27. package/dist/hub/lb-lifecycle.d.ts.map +1 -1
  28. package/dist/hub/lb-lifecycle.js +9 -17
  29. package/dist/hub/lb-lifecycle.js.map +1 -1
  30. package/dist/server/lb-express.d.ts.map +1 -1
  31. package/dist/server/lb-express.js +3 -3
  32. package/dist/server/lb-express.js.map +1 -1
  33. package/dist/server/lb-server.d.ts +35 -19
  34. package/dist/server/lb-server.d.ts.map +1 -1
  35. package/dist/server/lb-server.js +141 -88
  36. package/dist/server/lb-server.js.map +1 -1
  37. package/docs/binding-cases.md +150 -0
  38. package/docs/comparison.md +18 -29
  39. package/docs/miswarts.md +2 -2
  40. package/docs/reference/css.md +2 -15
  41. package/docs/reference/custom-elements.md +6 -8
  42. package/docs/reference/data-binding.md +37 -180
  43. package/docs/reference/names.md +10 -26
  44. package/docs/reference/page-files.md +1 -12
  45. package/docs/reference/server.md +0 -1
  46. package/docs/testing.md +2 -5
  47. package/package.json +1 -1
  48. package/skills/loadbare-app/SKILL.md +52 -54
  49. package/skills/loadbare-app/references/css.md +2 -15
  50. package/skills/loadbare-app/references/custom-elements.md +6 -8
  51. package/skills/loadbare-app/references/data-binding.md +37 -180
  52. package/skills/loadbare-app/references/miswarts.md +2 -2
  53. package/skills/loadbare-app/references/names.md +10 -26
  54. package/skills/loadbare-app/references/page-files.md +1 -12
  55. 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: (ctx: HubContext) => HubResult | Promise<HubResult>;
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-choices`,
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, filters
52
- * and takes `lb-min` and `lb-max` of that column read as its type rather
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 user's choices for the page, returned from `onPageEnter` to restore
103
- * what the user last chose: an order, a filter. Each column is a column of
104
- * `lb-choices`, and the hub places the page's rows by them as they land.
105
- * Return it beside `url()` with the spread both are written for:
106
- *
107
- * return { ...url({ acct: "23" }), ...choices({ sort: "-amount" }) };
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 choices(columns: Record<string, string>): HubData;
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 `choices()`, with `url()`
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 makes a choice, with every choice
192
- * made on the page, so an application can keep them for `onPageEnter` to
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, choices: Record<string, string>) => void | HubData | Promise<void | HubData>;
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 — see docs/reference/builder.md.
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":"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"}
1
+ {"version":3,"file":"lb-server.d.ts","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"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"}
@@ -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 { CHOICES_QUERY, COLUMN_TYPES, KIND_ROW, KIND_ROWS, LB_RESERVED_PREFIX, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
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 user's choices for the page, returned from `onPageEnter` to restore
68
- * what the user last chose: an order, a filter. Each column is a column of
69
- * `lb-choices`, and the hub places the page's rows by them as they land.
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 choices(columns) {
75
- return { [CHOICES_QUERY]: { ...columns } };
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
- // No row reaches the browser as null, which JSON keeps and
201
- // undefined it would drop.
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
- /** Run queries by name, each answering with all its rows or its row. */
223
- async function run(page, names, ctx) {
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 result = await query.run(ctx);
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, [CHOICES_QUERY]: chose, ...stated } = (await entry.requests.onPageEnter?.(ctx)) ?? {};
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(), choices() or nothing, ignoring them`);
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.row)))
304
+ if (item && !(parms && leadsHere(page, parms, oneRow(item))))
313
305
  return [item];
314
- // The choices go first, so the rows that read them land by them.
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
- ...choicesItem(page, chose),
317
- ...items(page, await run(page, Object.keys(entry.queries), ctx), true),
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
- // The user's choices, told to the page. One with no `onChoices` is
336
- // not keeping them, and is not told.
337
- if (fields.query === CHOICES_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
- return settle(page, {
343
- run: (c, r) => onChoices(c, r.values ?? {}),
344
- refresh: [],
345
- }, fields, ctx, "");
346
- }
347
- if (fields.keys !== undefined &&
348
- (name === REQUEST_ROW_UPDATE || name === REQUEST_ROW_DELETE)) {
349
- return eachKey(page, name, fields, ctx);
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
- row: moved,
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-choices` item for the choices `onPageEnter` returned, or nothing
424
- * when it returned none or ones that cannot be used, which are reported.
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 choicesItem(page, chose) {
427
- if (chose === undefined)
449
+ function sortItem(orders) {
450
+ if (orders.size === 0)
428
451
  return [];
429
- const refused = unusableUrl(chose, false);
430
- if (refused !== undefined) {
431
- console.warn(`loadbare: page '${page}' returned ${CHOICES_QUERY} ${refused}, ` +
432
- `ignoring it`);
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: CHOICES_QUERY,
438
- kind: KIND_ROW,
439
- key: URL_COLUMN_PATH,
440
- row: chose,
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` or `lb-choices` cannot be used, or nothing
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