@xeplr/ui-factory 1.7.1 → 1.7.2

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/AUTHORING.md CHANGED
@@ -15,7 +15,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
15
15
  | `employee-edit.screen.json` | the **add / edit form** the list opens in a popup | browser |
16
16
  | `EditEmployee.jsx` | the form's page, holding the **front-end hooks** class (`EmployeeHooks extends FactoryHooks`) | browser |
17
17
  | `EmployeeList.jsx` | the list's page — uses `EditEmployee.jsx`'s hooks | browser |
18
- | `employee.hooks.js` | **server hooks**: `save` / `get` / `delete` × `before` / `after` / `error` / `override` | server |
18
+ | `employee.functions.js` | **server functions**, registered with `@xeplr/actions` by event (`beforeLoad`, `beforeSave`, `afterSave`, `beforeDelete`, `onError`) and named in the screen's `events` | server |
19
19
  | `employee.model.js` | **server model**: `EmployeeModel extends FactoryModel` — getters / setters for values | server |
20
20
 
21
21
  `npx xeplr-factory screens employee.entity.json -o <dir>` writes all of them from the spec (`--no-pages` leaves out the two `.jsx`). The hooks and the model start as defaults — every method calling `super` — and stay that way unless the request needs more.
@@ -26,7 +26,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
26
26
  |---|---|
27
27
  | a field, a label, a rule, a dropdown, the layout, a colour | the screen JSON (then validate) |
28
28
  | "show / hide / filter rows on screen", "an extra button on each row", "fill in X before saving" (convenience) | front-end hooks in `EditEmployee.jsx` |
29
- | "must", "only if", "check against…", "email when…", "only managers see…" | server hooks in `employee.hooks.js` |
29
+ | "must", "only if", "check against…", "email when…", "only managers see…" | a server function in `employee.functions.js`, named in the screen's `events` |
30
30
  | "store as…", "convert…", "comma-separated / array", "work out X from Y" (data shape) | server model in `employee.model.js` |
31
31
  | a query in your own server code | `factory.table('employees')` — this company, active rows, the model applied |
32
32
 
@@ -37,7 +37,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
37
37
  ```sh
38
38
  npx xeplr-factory screens employee.entity.json -o src/screens/employee
39
39
  ```
40
- This writes `employee-list.screen.json`, `employee-edit.screen.json`, `EmployeeList.jsx`, `EditEmployee.jsx` (which holds the front-end hooks — see below) and an empty `employee.hooks.js` (the server's). Wire the two pages into the app's menu next to two `<FactoryBuilder>` pages for designing them (Screen · List, Screen · Edit, Designer · List, Designer · Edit). It refuses to overwrite existing files — they may have been refined in the designer — unless given `--force`, which replaces the screen JSON only: the `.jsx` pages and the hooks file are the app's code and are always kept.
40
+ This writes `employee-list.screen.json`, `employee-edit.screen.json`, `EmployeeList.jsx`, `EditEmployee.jsx` (which holds the front-end hooks — see below) and `employee.functions.js` (the server's functions, none registered yet). Wire the two pages into the app's menu next to two `<FactoryBuilder>` pages for designing them (Screen · List, Screen · Edit, Designer · List, Designer · Edit). It refuses to overwrite existing files — they may have been refined in the designer — unless given `--force`, which replaces the screen JSON only: the `.jsx` pages, the functions and the model are the app's code and are always kept.
41
41
 
42
42
  A single screen that is not an entity (a settings form, say) uses `npx xeplr-factory generate spec.json -o screen.json` with the screen spec below.
43
43
  3. **The table comes from Publish.** Records live in a **real table** — `employees`, one column per field — never in JSON, and there are no migration files to write: **publishing** the edit screen creates its table (`CREATE TABLE`, a column per field, real foreign keys for table dropdowns, the standard `id` / `isActive` / `mtId1–4` / audit columns), and publishing a later version changes it:
@@ -89,7 +89,7 @@ A request for "a form for employees" is an **entity**. Every entity is the same
89
89
  ```
90
90
 
91
91
  Setters run on every write (after `save.before`, before the screen's rules), getters on every read, on the routes and in `factory.table()`. Register it: `factory.init({ knex, hooks, models: [require('./employee.model')] })`.
92
- 8. **Server hooks, if the request needs them.** Anything beyond storing what the form shows — a value worked out on save, a check against other records, an email afterwards, a list narrowed to the user — goes in `employee.hooks.js`, never in the screen: `save`, `get` (list and one record) and `delete`, each with `before`, `after`, `error` and `override`. Register it with `factory.init({ hooks: { employee_edit: require('./employee.hooks') } })`. `override` replaces the whole operation — rules and the other hooks included — so use it only when the generic save / get / delete does not apply at all. The `screens` command never overwrites an existing hooks file, even with `--force`. Details: the [`@xeplr/factory` README](https://www.npmjs.com/package/@xeplr/factory#hooks).
92
+ 8. **Server functions, if the request needs them.** Anything beyond storing what the form shows — a value worked out on save, a check against other records, an email afterwards, a list narrowed to the user — is a function in `employee.functions.js`, never in the screen: `register({ name, event, description, execute })` with `@xeplr/actions`, then name it in `employee-edit.screen.json`'s `events` (`{ "beforeSave": "employeeBeforeSave" }`). Register before naming — a screen naming a function nobody registered is refused at publish. The app loads the file from its `functions/index.js`. The `screens` command never overwrites an existing functions file, even with `--force`. Details: the [`@xeplr/factory` README](https://www.npmjs.com/package/@xeplr/factory#events--the-apps-code-picked-by-name-in-the-screen).
93
93
  9. **Hand over.** The person opens the screens in the designer to move, resize and restyle controls.
94
94
 
95
95
  Do **not** add a button control for saving: every screen's footer already has **Save** (an AJAX call — never a form submit) and **Cancel** where it can go back. If the request mentions seeing or editing what was entered ("…and show the employees below"), add a `list`.
package/README.md CHANGED
@@ -17,13 +17,13 @@ It is built to work with Claude: ask for *"a form for employees with name, email
17
17
  npx xeplr-factory screens employee.entity.json -o src/screens/employee
18
18
  # employee-list.screen.json EmployeeList.jsx the list page — uses EditEmployee.jsx's hooks
19
19
  # employee-edit.screen.json EditEmployee.jsx the form page, holding the FRONT-END hooks (EmployeeHooks extends FactoryHooks)
20
- # employee.hooks.js SERVER hooks: save / get / delete × before / after / error / override
20
+ # employee.functions.js SERVER functions, registered by event, named in the screen's events
21
21
  # employee.model.js SERVER model: EmployeeModel extends FactoryModel — getters / setters for values
22
22
  ```
23
23
 
24
24
  `--no-pages` leaves out the two `.jsx` (for an app whose pages live elsewhere). `--force` replaces only the screen JSON: the pages, hooks and model are your code and are always kept.
25
25
 
26
- **Rules:** records live in real tables, never JSON · a form's **key** (`employee`) names its screens and table and never changes once published; its **label** (the screens' name) is renamed any time · rules that must hold go in server hooks, never front-end hooks.
26
+ **Rules:** records live in real tables, never JSON · a form's **key** (`employee`) names its screens and table and never changes once published; its **label** (the screens' name) is renamed any time · rules that must hold go in server functions (named in the screen's `events`), never front-end hooks.
27
27
 
28
28
  See [examples/](./examples) for exactly what it writes.
29
29
 
@@ -731,7 +731,7 @@ const screen = screenFromSpec({
731
731
  It lays the fields out as a tidy one- or two-column form and returns a checked document. The same is available on the command line:
732
732
 
733
733
  ```sh
734
- npx xeplr-factory screens entity.json -o dir # an entity → list + edit screens, pages, server hooks + model (--no-pages, --force)
734
+ npx xeplr-factory screens entity.json -o dir # an entity → list + edit screens, pages, server functions + model (--no-pages, --force)
735
735
  npx xeplr-factory migration edit.screen.json # preview the SQL Publish would run
736
736
  npx xeplr-factory generate spec.json -o screen.json # one screen from a spec
737
737
  npx xeplr-factory validate screen.json # every problem, with its path
@@ -807,7 +807,7 @@ src/
807
807
  validateDocument.js ─ the document checker
808
808
  values.js ─ values, validation, whether Save can go out, records and list columns, formSchema
809
809
  generate.js ─ screenFromSpec, screensFromSpec (list + edit), entity names
810
- scaffold.js ─ an entity → its screen JSON, .jsx pages (with front-end hooks), server hooks + model stubs
810
+ scaffold.js ─ an entity → its screen JSON, .jsx pages (with front-end hooks), server functions + model stubs
811
811
  hooks.js ─ FactoryHooks: get / save / delete / actions — extend and call super
812
812
  presets.js ─ ready-made fields (Email, Age, Gender…), changing a field's kind, label suggestions
813
813
  tableSchema.js ─ a form → its table; the publish plan (create / add / widen / confirmed drops and conversions)
@@ -5,7 +5,7 @@
5
5
  // turn a short spec into a laid-out document, and check a document before
6
6
  // handing it to an app, with errors precise enough to fix without guessing.
7
7
  //
8
- // xeplr-factory screens entity.json [-o dir] an entity → list + edit screens, their .jsx pages, server hooks + model stubs
8
+ // xeplr-factory screens entity.json [-o dir] an entity → list + edit screens, their .jsx pages, server functions + model stubs
9
9
  // xeplr-factory migration edit.screen.json [--from previous.screen.json] [-o migrations/]
10
10
  // the SQL that makes the table match the form
11
11
  // xeplr-factory generate spec.json [-o screen.json] spec → laid-out document
@@ -81,11 +81,11 @@ switch (cmd) {
81
81
  } catch (err) {
82
82
  fail(err.message)
83
83
  }
84
- // The app's own code — the server hooks, and the pages with their front-end
85
- // hooks — is kept as it is, even with --force. Only the screen JSON is replaced.
84
+ // The app's own code — the server functions and model, and the pages with
85
+ // their front-end hooks — is kept as it is, even with --force. Only the screen JSON is replaced.
86
86
  // --no-pages: only the screens and the server files — for an app whose pages live elsewhere (ui/src/pages).
87
87
  if (args.includes('--no-pages')) Object.keys(result.files).filter((f) => f.endsWith('.jsx')).forEach((f) => delete result.files[f])
88
- const kept = Object.keys(result.files).filter((f) => (f.endsWith('.hooks.js') || f.endsWith('.model.js') || f.endsWith('.jsx')) && existsSync(path.join(dir, f)))
88
+ const kept = Object.keys(result.files).filter((f) => (f.endsWith('.functions.js') || f.endsWith('.model.js') || f.endsWith('.jsx')) && existsSync(path.join(dir, f)))
89
89
  const writing = Object.keys(result.files).filter((f) => !kept.includes(f))
90
90
  const targets = writing.map((f) => path.join(dir, f))
91
91
  // Never overwrite a screen someone has since refined in the designer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/ui-factory",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, run them as auto-saving forms with lists of saved records — built for Claude to draft and people to refine",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/scaffold.js CHANGED
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // employee-list.screen.json EmployeeList.jsx — the list; Edit / New open a popup
7
7
  // employee-edit.screen.json EditEmployee.jsx — the add / edit form, also a page on its own
8
- // employee.hooks.js — the server's hooks for its records (all empty)
8
+ // employee.functions.js — the code its events can run, registered by event (none yet)
9
9
  // employee.model.js — the server's model: getters / setters for its values (none yet)
10
10
  //
11
11
  // The pages are thin on purpose: a screen document and the app's data calls,
@@ -31,7 +31,7 @@ export function scaffoldEntity(spec, controls) {
31
31
  [editJson]: JSON.stringify(edit, null, 2) + '\n',
32
32
  [`${names.listComponent}.jsx`]: listPage(names, listJson, editJson, list, edit),
33
33
  [`${names.editComponent}.jsx`]: editPage(names, editJson, edit),
34
- [`${names.file}.hooks.js`]: hooksFile(names, edit),
34
+ [`${names.file}.functions.js`]: functionsFile(names, edit),
35
35
  [`${names.file}.model.js`]: modelFile(names, edit)
36
36
  }
37
37
  return { files, names, list, edit }
@@ -66,44 +66,40 @@ export default function ${n.listComponent}({ api }) {
66
66
  `
67
67
  }
68
68
 
69
- function hooksFile(n, edit) {
70
- return `// Server hooks for ${n.singular} records — screen "${edit.id}" (and "${n.key}_list", which edits in it).
69
+ function functionsFile(n, edit) {
70
+ return `// Functions for ${n.singular} records — the code the "${edit.id}" screen's events run.
71
71
  //
72
- // Register with @xeplr/factory:
73
- // factory.init({ knex, hooks: { ${edit.id}: require('./${n.file}.hooks') } })
72
+ // Each is registered with @xeplr/actions under the EVENT it is for, and runs
73
+ // once the screen NAMES it — in ${n.file}-edit.screen.json's "events", or in the
74
+ // designer (Configure UI → Flows → a step → Events). A screen naming a function
75
+ // nobody registered is refused when it is published, so register first.
74
76
  //
75
- // Each operation has four hooks. Delete the ones you do not need.
76
- // before(ctx) first — save: return new values to replace them (they are then checked
77
- // against the screen's rules); get: narrow ctx.query; any: ctx.reject(message, { field })
78
- // after(ctx) once it succeeded — ctx.id, ctx.result; return a value to replace the result.
79
- // If it throws, the operation still happened (and error is told).
80
- // error(ctx, err) when the operation failed
81
- // override(ctx) does the WHOLE operation instead — no rules, no before / after / error,
82
- // no generic query. What it returns is the response.
77
+ // Load this file once at startup, before the screens: require it from the
78
+ // app's functions/index.js.
83
79
  //
84
- // ctx: op, screenKey, screen, table, id, isNew (save), many (get), input (what the UI sent, read-only),
85
- // values (save), previous (save / delete: the row before), result, user, tenant, knex, query (get), reject
86
-
87
- module.exports = {
88
- save: {
89
- // before: async function(ctx) { return ctx.values },
90
- // after: async function(ctx) {},
91
- // error: async function(ctx, err) {},
92
- // override: async function(ctx) { return savedRecord },
93
- },
94
- get: {
95
- // before: async function(ctx) { /* ctx.query.where(...) */ },
96
- // after: async function(ctx) { return ctx.result },
97
- // error: async function(ctx, err) {},
98
- // override: async function(ctx) { return ctx.id ? record : records },
99
- },
100
- delete: {
101
- // before: async function(ctx) {},
102
- // after: async function(ctx) {},
103
- // error: async function(ctx, err) {},
104
- // override: async function(ctx) { return { id: ctx.id } },
105
- }
106
- }
80
+ // event runs return
81
+ // beforeLoad before records are read nothing, or a narrower ctx.query
82
+ // beforeSave before a record is written the values to save
83
+ // afterSave once it is saved (ignored)
84
+ // beforeDelete before a delete nothing
85
+ // onError when load / save / delete failed (ignored)
86
+ //
87
+ // ctx: op, screenKey, screen, table, id, isNew (save), many (get), input (what the
88
+ // UI sent, read-only), values (save), previous (save / delete: the row
89
+ // before), result (afterSave), error (onError), query (beforeLoad), user,
90
+ // tenant, knex, db() (this form's table, scoped), reject(message, { field })
91
+
92
+ var { register } = require('@xeplr/actions');
93
+
94
+ // register({
95
+ // name: '${n.key}BeforeSave',
96
+ // event: 'beforeSave',
97
+ // description: 'What it checks or changes, in one sentence — the designer shows it.',
98
+ // execute: function(ctx) {
99
+ // // ctx.reject('Explain what is wrong', { field: 'someField' })
100
+ // return ctx.values;
101
+ // }
102
+ // });
107
103
  `
108
104
  }
109
105
 
@@ -111,10 +107,10 @@ function modelFile(n, edit) {
111
107
  return `// The model for ${n.singular} records — how values are shaped between your code and
112
108
  // the "${edit.source}" table, like Sequelize's getters and setters.
113
109
  //
114
- // Register with @xeplr/factory: factory.init({ knex, hooks, models: [require('./${n.file}.model')] })
110
+ // Register with @xeplr/factory: factory.init({ knex, models: [require('./${n.file}.model')] })
115
111
  //
116
112
  // Used everywhere: the screens' routes and factory.table('${edit.source}'). Models shape
117
- // data; the hooks file decides behaviour.
113
+ // data; the functions its events name decide behaviour.
118
114
 
119
115
  var { FactoryModel } = require('@xeplr/factory');
120
116
 
@@ -159,7 +155,7 @@ import editScreen from './${editJson}'
159
155
  //
160
156
  // ctx.screen says which screen is calling ("${n.key}_list" or "${edit.id}").
161
157
  // Keep them as methods (save(values, ctx) { … }) — super does not work in
162
- // arrow functions. Rules that must hold belong in the server's hooks.
158
+ // arrow functions. Rules that must hold belong in the server's functions (${n.file}.functions.js).
163
159
  export class ${n.hooksClass} extends FactoryHooks {
164
160
  /** A list's rows (ctx.many), or the record Edit opens (ctx.id). */
165
161
  get(ctx) {