@loadbare/app 0.7.3 → 0.8.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.map +1 -1
- package/dist/build/assemble.js +67 -6
- package/dist/build/assemble.js.map +1 -1
- package/dist/core/lb-constants.d.ts +2 -2
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +28 -14
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +4 -10
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +2 -2
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +98 -8
- 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 +61 -26
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +2 -7
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +8 -15
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +0 -3
- package/dist/server/lb-server.js.map +1 -1
- package/docs/TECHREF-1.0.md +141 -100
- package/docs/analysis-closed-set.md +210 -0
- package/docs/comparison.md +1124 -0
- package/docs/prior-art.md +216 -0
- package/docs/reference/custom-elements.md +21 -6
- package/docs/reference/data-binding.md +120 -40
- package/docs/reference/page-files.md +13 -9
- package/docs/reference/widgets.md +2 -2
- package/docs/roadmap.md +1 -1
- package/docs/testing.md +7 -1
- package/docs/theory.md +737 -408
- package/docs/tutorials/080-widget-requests.md +9 -28
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-express.js","sourceRoot":"","sources":["../../server/lb-express.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,OAAsC,MAAM,SAAS,CAAC;AAC7D,OAAO,EACL,
|
|
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=<name> 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 query string rather than in the body, which is what\n * lets the operation set stay closed. Every hub call is a POST, so an\n * application's own GET routes never collide with this one and no page name\n * is reserved.\n */\n\nimport express, { type Request, type Router } 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 */\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, async (req, res) => {\n const page = String(req.query.page ?? \"\");\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 return router;\n}\n"]}
|
|
@@ -94,17 +94,13 @@ export interface CrudOp<W> {
|
|
|
94
94
|
run: (ctx: HubContext, where: W) => void | HubData | Promise<void | HubData>;
|
|
95
95
|
refresh: string[];
|
|
96
96
|
}
|
|
97
|
-
export type CellChangeOp = CrudOp<{
|
|
98
|
-
key: string;
|
|
99
|
-
cell: string;
|
|
100
|
-
value: string;
|
|
101
|
-
}>;
|
|
102
97
|
export type RowDeleteOp = CrudOp<{
|
|
103
98
|
key: string;
|
|
104
99
|
}>;
|
|
105
100
|
export type RowInsertOp = CrudOp<{
|
|
106
101
|
values: Record<string, string>;
|
|
107
102
|
}>;
|
|
103
|
+
/** `values` holds only the columns being set; see `Hub.runRowUpdate`. */
|
|
108
104
|
export type RowUpdateOp = CrudOp<{
|
|
109
105
|
key: string;
|
|
110
106
|
values: Record<string, string>;
|
|
@@ -117,11 +113,10 @@ export type RowUpdateOp = CrudOp<{
|
|
|
117
113
|
*
|
|
118
114
|
* Every key here is the reserved `lb-action` value with the prefix stripped
|
|
119
115
|
* and the rest camel-cased, so the attribute, the wire field and this key
|
|
120
|
-
* are one vocabulary. All
|
|
116
|
+
* are one vocabulary. All three are list operations: each needs a key, and a
|
|
121
117
|
* key exists only on a live row inside a list.
|
|
122
118
|
*/
|
|
123
119
|
export interface Crud {
|
|
124
|
-
cellChange?: CellChangeOp;
|
|
125
120
|
rowDelete?: RowDeleteOp;
|
|
126
121
|
rowInsert?: RowInsertOp;
|
|
127
122
|
rowUpdate?: RowUpdateOp;
|
|
@@ -165,13 +160,6 @@ export interface Hub {
|
|
|
165
160
|
* set declared with it.
|
|
166
161
|
*/
|
|
167
162
|
runAction(page: string, name: string, where: Where, ctx: HubContext): Promise<HubData>;
|
|
168
|
-
/** Edit one cell: run the list's declared `cellChange`, then its refresh set. */
|
|
169
|
-
runCellChange(page: string, where: {
|
|
170
|
-
list: string;
|
|
171
|
-
key: string;
|
|
172
|
-
cell: string;
|
|
173
|
-
value: string;
|
|
174
|
-
}, ctx: HubContext): Promise<HubData>;
|
|
175
163
|
/** Drop one row: run the list's declared `rowDelete`, then its refresh set. */
|
|
176
164
|
runRowDelete(page: string, where: {
|
|
177
165
|
list: string;
|
|
@@ -182,7 +170,12 @@ export interface Hub {
|
|
|
182
170
|
list: string;
|
|
183
171
|
values: Record<string, string>;
|
|
184
172
|
}, ctx: HubContext): Promise<HubData>;
|
|
185
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Edit a row: run the list's declared `rowUpdate`, then its refresh set.
|
|
175
|
+
* `values` holds only the columns being set — one from a widget that
|
|
176
|
+
* commits a cell, several from a form — and a column absent from it is left
|
|
177
|
+
* as it is, as an SQL UPDATE leaves it.
|
|
178
|
+
*/
|
|
186
179
|
runRowUpdate(page: string, where: {
|
|
187
180
|
list: string;
|
|
188
181
|
key: string;
|
|
@@ -1 +1 @@
|
|
|
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;;;;;;;;;;;;;;;GAeG;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,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,
|
|
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
|
@@ -100,9 +100,6 @@ export function createHub(pages) {
|
|
|
100
100
|
runAction(page, name, where, ctx) {
|
|
101
101
|
return settle(page, pages[page]?.requests.actions?.[name], where, ctx, `loadbare: page '${page}' declares no action '${name}'`);
|
|
102
102
|
},
|
|
103
|
-
runCellChange(page, where, ctx) {
|
|
104
|
-
return settle(page, pages[page]?.requests.crud?.[where.list]?.cellChange, { key: where.key, cell: where.cell, value: where.value }, ctx, `loadbare: page '${page}' declares no cellChange for '${where.list}'`);
|
|
105
|
-
},
|
|
106
103
|
runRowDelete(page, where, ctx) {
|
|
107
104
|
return settle(page, pages[page]?.requests.crud?.[where.list]?.rowDelete, { key: where.key }, ctx, `loadbare: page '${page}' declares no rowDelete for '${where.list}'`);
|
|
108
105
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-server.js","sourceRoot":"","sources":["../../server/lb-server.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,oEAAoE;AACpE,4EAA4E;AAC5E,0EAA0E;AAC1E,6CAA6C;AAG7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAmC7D,yCAAyC;AACzC,MAAM,UAAU,GAAG,CAAC,GAA4C;IAC9D,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAAC,GAAgD;IACnE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,KAAK,CAAC,MAAyC;IAC7D,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC;AACvB,CAAC;AAyJD;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,sEAAsE;IACtE,yEAAyE;IACzE,sEAAsE;IACtE,qBAAqB;IACrB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG;YACf,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YAC7B,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC;SAC7C,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,IAAI,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,KAAK,CACb,mBAAmB,IAAI,eAAe,IAAI,eAAe;oBACvD,mBAAmB,kBAAkB,gBAAgB,CACxD,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,UAAU,GAAG,CAChB,IAAY,EACZ,KAAe,EACf,GAAe;QAEf,MAAM,IAAI,GAAY,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,CAAC,IAAI,CAAC,mBAAmB,IAAI,wBAAwB,IAAI,GAAG,CAAC,CAAC;gBACrE,SAAS;YACX,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACpC,uEAAuE;YACvE,uEAAuE;YACvE,sEAAsE;YACtE,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,EAAE,CAAC;gBACtD,OAAO,CAAC,IAAI,CACV,oBAAoB,IAAI,iBAAiB,KAAK,CAAC,IAAI,gBAAgB;oBACjE,QAAQ,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CACvD,CAAC;gBACF,SAAS;YACX,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;QACtB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACH,KAAK,UAAU,MAAM,CACnB,IAAY,EACZ,QAAwC,EACxC,KAAQ,EACR,GAAe,EACf,QAAgB;QAEhB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAc,CAAC,CAAC;QACvD,OAAO,EAAE,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;IAC5E,CAAC;IAED,OAAO;QACL,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG;YACzB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,CAAC,IAAI,CAAC,sBAAsB,IAAI,GAAG,CAAC,CAAC;gBAC5C,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,KAAK,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;YACxC,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;QACpD,CAAC;QAED,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG;YAC9B,OAAO,MAAM,CACX,IAAI,EACJ,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EACrC,KAAK,EACL,GAAG,EACH,mBAAmB,IAAI,yBAAyB,IAAI,GAAG,CACxD,CAAC;QACJ,CAAC;QAED,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG;YAC5B,OAAO,MAAM,CACX,IAAI,EACJ,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,UAAU,EACpD,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EACxD,GAAG,EACH,mBAAmB,IAAI,iCAAiC,KAAK,CAAC,IAAI,GAAG,CACtE,CAAC;QACJ,CAAC;QAED,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG;YAC3B,OAAO,MAAM,CACX,IAAI,EACJ,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EACnD,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,EAClB,GAAG,EACH,mBAAmB,IAAI,gCAAgC,KAAK,CAAC,IAAI,GAAG,CACrE,CAAC;QACJ,CAAC;QAED,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG;YAC3B,OAAO,MAAM,CACX,IAAI,EACJ,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EACnD,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EACxB,GAAG,EACH,mBAAmB,IAAI,gCAAgC,KAAK,CAAC,IAAI,GAAG,CACrE,CAAC;QACJ,CAAC;QAED,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG;YAC3B,OAAO,MAAM,CACX,IAAI,EACJ,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EACnD,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EACxC,GAAG,EACH,mBAAmB,IAAI,gCAAgC,KAAK,CAAC,IAAI,GAAG,CACrE,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","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 */\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 CellChangeOp = CrudOp<{ key: string; cell: string; value: string }>;\nexport type RowDeleteOp = CrudOp<{ key: string }>;\nexport type RowInsertOp = CrudOp<{ values: Record<string, string> }>;\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 four 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 cellChange?: CellChangeOp;\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 /** Edit one cell: run the list's declared `cellChange`, then its refresh set. */\n runCellChange(\n page: string,\n where: { list: string; key: string; cell: string; value: string },\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 /** Edit several cells at once: run the list's declared `rowUpdate`, then its refresh set. */\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 runCellChange(page, where, ctx) {\n return settle(\n page,\n pages[page]?.requests.crud?.[where.list]?.cellChange,\n { key: where.key, cell: where.cell, value: where.value },\n ctx,\n `loadbare: page '${page}' declares no cellChange for '${where.list}'`,\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 } 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 */\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"]}
|