pgkiln 0.55.0 → 0.57.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 (53) hide show
  1. package/SECURITY.md +2 -0
  2. package/docs/apex-feature-parity.md +4 -4
  3. package/docs/guide/03-builder.md +11 -4
  4. package/docs/guide/04-pages-and-regions.md +26 -0
  5. package/docs/guide/12-development.md +14 -0
  6. package/examples/gallery/README.md +4 -0
  7. package/examples/gallery/gallery.json +8 -0
  8. package/examples/gallery/samples/sample-approvals.sql +153 -0
  9. package/examples/gallery/samples/sample-data-loading.sql +142 -0
  10. package/examples/gallery/samples/sample-files.sql +160 -0
  11. package/examples/gallery/samples/sample-maps.sql +175 -0
  12. package/examples/gallery/samples/sample-reporting.sql +4 -3
  13. package/package.json +1 -1
  14. package/public/builder.css +1 -1
  15. package/public/worksheet.js +31 -26
  16. package/src/builder/i18n/de.ts +371 -0
  17. package/src/builder/i18n/en.ts +371 -0
  18. package/src/builder/i18n/es.ts +371 -0
  19. package/src/builder/i18n/fr.ts +371 -0
  20. package/src/builder/i18n/nl.ts +371 -0
  21. package/src/builder/i18n/pt.ts +371 -0
  22. package/src/builder/i18n.ts +10 -0
  23. package/src/builder/object-browser.ts +71 -67
  24. package/src/builder/region-help.ts +1 -1
  25. package/src/builder/region-settings.ts +3 -1
  26. package/src/builder/table-editor.ts +51 -48
  27. package/src/builder/ui.ts +3 -2
  28. package/src/builder/worksheet.ts +160 -154
  29. package/src/i18n/ar.ts +1 -0
  30. package/src/i18n/cs.ts +1 -0
  31. package/src/i18n/da.ts +1 -0
  32. package/src/i18n/de.ts +1 -0
  33. package/src/i18n/el.ts +1 -0
  34. package/src/i18n/es.ts +1 -0
  35. package/src/i18n/fi.ts +1 -0
  36. package/src/i18n/fr.ts +1 -0
  37. package/src/i18n/he.ts +1 -0
  38. package/src/i18n/it.ts +1 -0
  39. package/src/i18n/ja.ts +1 -0
  40. package/src/i18n/ko.ts +1 -0
  41. package/src/i18n/nb.ts +1 -0
  42. package/src/i18n/pl.ts +1 -0
  43. package/src/i18n/pt.ts +1 -0
  44. package/src/i18n/ru.ts +1 -0
  45. package/src/i18n/sv.ts +1 -0
  46. package/src/i18n/tr.ts +1 -0
  47. package/src/i18n/uk.ts +1 -0
  48. package/src/i18n/zh.ts +1 -0
  49. package/src/i18n.ts +2 -0
  50. package/src/runtime/ai-filter.ts +22 -6
  51. package/src/runtime/ask-facets.ts +72 -0
  52. package/src/runtime/ir-plan.ts +17 -3
  53. package/src/runtime/smart-filters.ts +36 -2
package/SECURITY.md CHANGED
@@ -110,6 +110,8 @@ first version (MVP). Every finding below has a regression test in
110
110
  | Data security per table (sprint 119) | SQL Workshop → a table → Security writes `GRANT` / `REVOKE` and a masking view, so it needs a signed-in developer and the CSRF token, and works only on tables of the workspace's schemas (never pgkiln's own or the system's: 404). Table, column, view and role names reach SQL quoted; view names and application roles that could break out of their quotes are refused. The masking role is `nologin`, a member of the reader roles, so row level security still applies inside the view, and the generated SQL refuses to run when that role would bypass RLS. MCP `run_query` with an application runs as that application's database role (read-only) and refuses unknown applications. |
111
111
  | App Gallery (sprint 120) | Installing a sample app runs shipped SQL as the owner, so only administrators may, with the CSRF token, into their current workspace; a request names only a key of `gallery.json` of its own kind (sample, starter, utility), never a path, and the file names in the catalogue must match fixed patterns. The script and the workspace placement run in one transaction: a failing script leaves nothing behind. Utility apps are blueprints: any developer installs one into their own workspace, only after the review. An installed sample is edited only from its own workspace. The search term is escaped. |
112
112
  | Builder language (sprint 121) | Only the codes of the builder's languages are accepted: anything else on a sign-in link is ignored (also a repeated parameter), the status bar's menu refuses it (400) and checks the CSRF token. A sign-in link sets only an HttpOnly, SameSite=Lax cookie that the sign-in page reads; a developer's stored choice changes only through their own signed-in session. Translated texts are escaped like the English ones; placeholders are filled with escaped values unless the code passes markup on purpose (`tx.rich`). |
113
+ | Ask and smart filters set facets (sprint 122) | A phrase can only set facets of the faceted search and smart filters regions on the page that the user sees, on their configured columns; values and range bounds become the facets' own URL parameters (bounds checked as numbers or dates) and reach SQL as query parameters, never as text. Values in a phrase are looked up in the report's rows with a query parameter, as the application's role. A facet chip's Remove is checked against the chips the form showed (a tampered key is refused). Sample apps keep their demo accounts with known passwords: install them on development or demo instances only. |
114
+ | Builder texts in the browser (sprint 123) | The builder's JavaScript gets its texts as JSON in an attribute (`<div id="builder-texts" data-texts>`), escaped like any attribute value, so builder pages still have no inline script. The texts are the fixed message files, never data; scripts put them into the page with `textContent` and attributes, not as HTML. |
113
115
 
114
116
  ## Findings of the MVP review and their fixes
115
117
 
@@ -213,7 +213,7 @@ the 26.2 comparison.)
213
213
  | Approvals and task list | ✅ | Task definitions (approval / action, owner roles and users, business administrators, priority, due date, details page), `meta.create_task` from application SQL, a task list region (claim, approve/reject/complete with comment, release, delegate, cancel, history), completion SQL in the same transaction. (sprint 64) **Vacation rules** (a stand-in acts on a user's tasks while the rule is active, checked on every request) and **expiration policies** (expire, renew up to N times, escalate to a role once; a waiting workflow goes on with the outcome `expired`). (sprint 114) **custom outcomes** with labels and colours (Yes / No / Undecided), **task events** on complete, expire and a failed completion SQL (SQL, e-mail or start a workflow; reopen or keep the task) and **notifications** from e-mail templates on create, complete and expire ([chapter 6](guide/06-processing.md#custom-outcomes)) |
214
214
  | Workflow (26.1: parallel flows, multi-tenancy) | ✅ | Workflow definitions of task, SQL, switch, wait, **invoke API** (a REST data source or URL through the invoke-API code, response values and the HTTP status into variables, retryable faults, no transaction open during the call) and end steps with variables, started from application SQL, run by the server (NOTIFY + polling) as the app's role; console region (terminate, retry a faulted step); diagram in the builder. **Parallel branches** (split, then a join that waits for all or for the first branch) and **versions** (development, active, inactive; running instances keep theirs) ([chapter 6](guide/06-processing.md#workflows)). (0.31) **Tenants**: `meta.set_tenant()` (APEX: `APEX_SESSION.SET_TENANT_ID`); workflows, their tasks, background chains, the task list, the console and every action follow the session's tenant ([chapter 6](guide/06-processing.md#tenants)). (sprint 114, 26.1 / 26.2) **Send E-Mail** steps (a template or subject and body), **draft** steps allowed only in development versions, branching on custom task outcomes, **migrating running instances** to a newer version (step mapping, preview, one transaction, a log; not yet for instances inside open parallel branches: [chapter 6](guide/06-processing.md#migrating-instances)) and the diagram as **SVG / PNG** |
215
215
  | Automations (scheduled) | ✅ | Shared Components → Automations: cron schedules with time zones, once or per row of a query, **several ordered actions** with their own conditions, **error handling** (stop, skip the row and continue, or disable; row errors in the run history), roles, run history and Run now, **on-demand runs from SQL** with `meta.run_automation()` (like `APEX_AUTOMATION.EXECUTE`, synchronous in the caller's transaction), safe with several servers ([chapter 6](guide/06-processing.md#automations)) |
216
- | AI assistant, natural-language reports (NL2IR), AI agents and tools (26.1) | ✅ | Region type **AI assistant**: a conversation per session, context queries (RAG over developer queries, run as the app's role, read-only), and **tools** the model may call: SQL with bound arguments checked against declared parameters (read-only unless marked as writing, row limit, timeout) and REST data sources, each optionally behind an authorization scheme. **AI Interactive Reports** (sprint 112; 26.1 / 26.2): an "Ask in your own words" box turns a question into the report's own settings (filters, highlights, search, sort, control break, totals, group by, pivot, chart and computed columns such as "bonus is 10% of salary"), never SQL, limited to its shown columns; each setting is a chip with Edit and Remove, plus Undo. Without an AI service a built-in phrase parser (English, Dutch, German) answers ([chapter 4](guide/04-pages-and-regions.md#ask-in-your-own-words-ai-interactive-reports)). Not yet: phrases mapped to faceted search and smart filter values. (sprint 111) **AI Agents** as a shared component (26.1 / 26.2): an AI service, system prompt, reasoning effort (low / medium / high, mapped per provider), approved tools, max steps and tokens and an authorization scheme; an assistant region names one. **AI Tools**: SQL (read-only transaction, the app role through RLS, timeout), PL/pgSQL function, REST data source, JavaScript action (a registered action or custom-event dynamic action), and **Navigate**: pgkiln builds the checksummed URL, only to pages the user may open. Every tool call is logged with its arguments (debug and Activity → AI usage). Providers add Gemini and OpenAI-compatible endpoints (Ollama, vLLM, Azure OpenAI). Answers are not streamed; no OCI provider. `pgvector` covers semantic search on the data side |
216
+ | AI assistant, natural-language reports (NL2IR), AI agents and tools (26.1) | ✅ | Region type **AI assistant**: a conversation per session, context queries (RAG over developer queries, run as the app's role, read-only), and **tools** the model may call: SQL with bound arguments checked against declared parameters (read-only unless marked as writing, row limit, timeout) and REST data sources, each optionally behind an authorization scheme. **AI Interactive Reports** (sprint 112; 26.1 / 26.2): an "Ask in your own words" box turns a question into the report's own settings (filters, highlights, search, sort, control break, totals, group by, pivot, chart and computed columns such as "bonus is 10% of salary"), never SQL, limited to its shown columns; each setting is a chip with Edit and Remove, plus Undo. Without an AI service a built-in phrase parser (English, Dutch, German) answers ([chapter 4](guide/04-pages-and-regions.md#ask-in-your-own-words-ai-interactive-reports)). (sprint 122) Conditions a facet can hold set the page's **faceted search and smart filters** facets (values, excluded values, custom ranges, stars), shown as chips; smart filters with `"ask": true` read a typed phrase and offer it as one suggestion. (sprint 111) **AI Agents** as a shared component (26.1 / 26.2): an AI service, system prompt, reasoning effort (low / medium / high, mapped per provider), approved tools, max steps and tokens and an authorization scheme; an assistant region names one. **AI Tools**: SQL (read-only transaction, the app role through RLS, timeout), PL/pgSQL function, REST data source, JavaScript action (a registered action or custom-event dynamic action), and **Navigate**: pgkiln builds the checksummed URL, only to pages the user may open. Every tool call is logged with its arguments (debug and Activity → AI usage). Providers add Gemini and OpenAI-compatible endpoints (Ollama, vLLM, Azure OpenAI). Answers are not streamed; no OCI provider. `pgvector` covers semantic search on the data side |
217
217
  | *Generate Text with AI* process, structured outputs (26.1) | ✅ | **AI services** (Workspace utilities, administrators): Claude through `@anthropic-ai/sdk` or OpenAI through `openai`, encrypted write-only keys (or `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`), per-app access with daily request and token limits, a usage log. Page process `ai_generate` (text into an item, or structured output into several items with a schema built from the items or your own) and a dynamic action that runs it without a page submit; `meta.ai_generate` / `ai_result` / `ai_available` from SQL ([chapter 6](guide/06-processing.md)). Prompts, item values included, go to the chosen provider |
218
218
  | Blueprints, spec-driven development (26.1) | ✅ | Create → **From a blueprint**: a JSON spec of tables (types, required, unique, allowed values, references), pages, navigation and sample data; optionally drafted by AI from a description; always reviewed (problems, SQL, pages, menu) before the application is created in one transaction; saved blueprints; (sprint 66) **a blueprint from an existing application** (its tables, pages, menu and optionally sample rows, with notes on what didn't carry over) |
219
219
 
@@ -247,10 +247,10 @@ row above where there is one.
247
247
  | WS-2 Data Reporter as a workspace tool for business users | ✅ | (sprint 110) App Builder → Data Reporter: datasets, reporting applications with editors and viewers, published reports with stable URLs ([Data and integration](#data-and-integration)) |
248
248
  | WS-3 Team Development: issues, labels, milestones, templates | ✅ | (sprint 113) Builder → Team Development with a board and a set-up wizard ([App Builder and development](#app-builder-and-development)) |
249
249
  | WS-4 AI Agents with tools | ✅ | (sprint 111) AI Agents and AI Tools as shared components, a Navigate tool, Gemini and OpenAI-compatible services ([Workflow, automation and AI](#workflow-automation-and-ai)) |
250
- | WS-5 AI interactive reports: chips, computed columns | ✅ | (sprint 112) *Ask in your own words* with chips, Undo and computed columns; a phrase parser that needs no AI service. Not yet: phrases mapped to facets and smart filters |
250
+ | WS-5 AI interactive reports: chips, computed columns | ✅ | (sprint 112) *Ask in your own words* with chips, Undo and computed columns; a phrase parser that needs no AI service; (sprint 122) phrases set faceted search and smart filter facets |
251
251
  | WS-6 Workflow 26.2: outcomes, migrating running instances, drafts, diagram export | ✅ | (sprint 114) Custom task outcomes, task events, Send E-Mail and draft steps, migrating running instances, the diagram as SVG / PNG ([Workflow, automation and AI](#workflow-automation-and-ai)) |
252
- | WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127). Translated: the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs; other screens move over incrementally (English until then). Builder texts are keyed message files with a coverage check in CI |
253
- | WS-8 App Gallery: more sample apps, Utility Apps | ✅ | (sprint 120) Four tabs with counts and a search that also works without JavaScript ([chapter 3](guide/03-builder.md#app-gallery)). *Sample Apps*: HR Demo, Tasks, six single-topic apps (Sample Charts, Sample Cards, Sample Calendar, Sample Interactive Grids, Sample Dynamic Actions, Sample Reporting) and the **UX Pattern Catalog**, each with its own schema, data, demo account and explanatory texts, installed by administrators from one SQL file in one transaction; *Utility Apps*: Lookup Lists, Release Notes and Announcements as blueprints any developer installs into the workspace. APEX's 27 sample apps include topics pgkiln shows in HR Demo instead (maps, approvals, workflows, REST); no remote catalogue |
252
+ | WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127). Translated: the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs; other screens move over incrementally (English until then). Builder texts are keyed message files with a coverage check in CI (sprint 123) Also the SQL Worksheet, the Object Browser and Table Properties, with the texts of the browser code. |
253
+ | WS-8 App Gallery: more sample apps, Utility Apps | ✅ | (sprint 120) Four tabs with counts and a search that also works without JavaScript ([chapter 3](guide/03-builder.md#app-gallery)). *Sample Apps*: HR Demo, Tasks, ten single-topic apps (Sample Charts, Sample Cards, Sample Calendar, Sample Interactive Grids, Sample Dynamic Actions, Sample Reporting; sprint 122: Sample Maps, Sample Approvals, Sample Data Loading, Sample Files) and the **UX Pattern Catalog**, each with its own schema, data, demo account and explanatory texts, installed by administrators from one SQL file in one transaction; *Utility Apps*: Lookup Lists, Release Notes and Announcements as blueprints any developer installs into the workspace. APEX's 27 sample apps include topics pgkiln shows in HR Demo instead (REST, trees, collections); no remote catalogue |
254
254
  | WS-9 Mail queue reporting, workspace SMTP, workspace network ACLs | ✅ | (sprint 118) Administration → Mail queue (per hour and per workspace / application, the slowest provider calls, failures with the provider's answer, Retry and Purge), a workspace's own mail service, and a workspace's network access list that narrows the installation's allow-list for every outgoing call ([Administration](#administration)) |
255
255
  | WS-10 Universal Theme 26.2: Pillar appearances, navigation polish | ✅ | (sprint 117) Theme Roller → Navigation appearance Pillar Light / Pillar Dark per application or style, with a header strip colour for light and dark mode; the current page filled, its parents bold with an accent guide line; `u-color-*-subtle` utilities; a lighter processing indicator; RTL mirrored |
256
256
  | WS-11 26.1 component details: infinite scrolling LOVs, Wait Popup, dynamic actions on menu entries | ✅ | (sprint 115) Popup LOVs that load on scroll, Wait Popup dynamic actions, the *Menu entry chosen* event ([Items](#items), [Logic and processing](#logic-and-processing)) |
@@ -274,8 +274,9 @@ device; other developers keep their own. It only changes the builder: your appli
274
274
 
275
275
  What is translated: the builder's frame (the rail, the top bar and its search, your account menu, the status bar,
276
276
  standard messages and error pages), the sign-in page, the Workspace home, the App Builder home, an application's
277
- home page and its tabs, the Page Designer's toolbar, menus, component tree, panes and gallery, and the SQL Workshop's
278
- home page and tabs. Other builder pages are still in English (they move over screen by screen); so are the
277
+ home page and its tabs, the Page Designer's toolbar, menus, component tree, panes and gallery, the SQL Workshop's
278
+ home page and tabs, and (sprint 123) the **SQL Worksheet** (with its messages in the browser), the **Object Browser**
279
+ and **Table Properties**. Other builder pages are still in English (they move over screen by screen); so are the
279
280
  Page Designer's property names and their help, the links to this guide, and any SQL the builder generates.
280
281
 
281
282
  ## Workspace home
@@ -367,8 +368,14 @@ server, `?q=`); every word must be in the title or the description.
367
368
  four views, create on click, drag and drop through a function), **Sample Interactive Grids** (an editable grid,
368
369
  select lists, totals, a frozen column, a row menu, master-detail, the grid's views), **Sample Dynamic Actions**
369
370
  (show and hide, values from the server, refresh, messages, a wait popup, classes, an inline dialog) and
370
- **Sample Reporting** (interactive report, faceted search, smart filters, a classic report with column options and
371
- a column template);
371
+ **Sample Reporting** (interactive report, faceted search, smart filters that also read a phrase, a classic report
372
+ with column options and a column template); and (sprint 122; APEX: Sample Maps, Sample Approvals, Sample Data
373
+ Loading, Sample File Upload and Download) **Sample Maps** (markers and clusters, layers with GeoJSON and a heat
374
+ map, a map that filters a list by distance, a location item, without PostGIS), **Sample Approvals** (a form
375
+ starts a workflow: approved by itself below 1,000, otherwise an approval task decided under My tasks; the
376
+ workflow console; accounts **demo** and **approver**), **Sample Data Loading** (`data_load` processes by column
377
+ name, through a data load definition and skipping bad rows, with example files to download) and **Sample Files**
378
+ (a file in the record's columns, several files in a child table, a photo cropped before upload);
372
379
  - the **UX Pattern Catalog** (APEX: the Universal Theme app): region templates and template options, button
373
380
  styles, item types and form layouts, alerts, badges, colour classes, lists, media lists, timelines, comments,
374
381
  avatars, metric cards, empty states, a dialog, a drawer and a side column, each named by its template, option
@@ -312,6 +312,23 @@ klerken*, *bonus is 10% van salaris*) and German (*Gehalt über 3000*, *sortiere
312
312
  absteigend*, *Summe von Gehalt pro Abteilung*, *nur Angestellte*) work the same way; another
313
313
  language adds a phrase table to `src/runtime/ir-phrases.ts`.
314
314
 
315
+ **With faceted search or smart filters** on the same report (sprint 122; APEX 26.2), a condition
316
+ that one of their facets can hold sets **that facet** instead of becoming a report filter, so the
317
+ facet panel or the smart filter chips show it and the facet counts follow. The facets' selections
318
+ are chips of the box too (with Remove; the facet region edits them), and Undo and *start over*
319
+ include them:
320
+
321
+ | Condition | Facet |
322
+ |---|---|
323
+ | *only clerks*, *clerks and analysts* (equals) | a checkbox facet: its values |
324
+ | *not clerks* (differs from) | a checkbox facet with `exclude`: the value, excluded |
325
+ | *salary between 1000 and 3000*, *at least 2000*, *hired in 1981* | a range facet with custom ranges (`custom`): from / to |
326
+ | *rating at least 4* | a star facet: *4 stars & up* |
327
+
328
+ Anything else (*salary over 3000*, *name contains*, a column without a facet) stays a report
329
+ filter. Only facets of regions the user sees count. A smart filters region can also read its own
330
+ search field this way (`"ask": true`, [below](#smart_filters)).
331
+
315
332
  **With an AI service** (`"ai_filter": {"service": "NAME"}`, an [AI
316
333
  service](06-processing.md#generate-text-with-ai) the application may use), the request goes to the
317
334
  service with the report's shown columns (names, headings and kinds: text, number, date, boolean),
@@ -950,6 +967,15 @@ that filter. If nothing matches, the term searches all columns of the report.
950
967
  | `facets` | As for [`facets`](#facets-faceted-search) (checkbox, range and star; `exclude` and `custom` are for the facets panel) |
951
968
  | `suggestions` | Suggestions per facet, 0–10 (default 3) |
952
969
  | `placeholder` | The text in the empty search field (translatable) |
970
+ | `ask` | `true`: also read the search text as a phrase (sprint 122; APEX 26.2) |
971
+
972
+ With `"ask": true` the text typed in the search field is also read as a phrase, with the built-in
973
+ phrases of [*Ask in your own words*](#ask-in-your-own-words-ai-interactive-reports) (no AI service
974
+ needed): *"clerks in sales with salary between 1000 and 3000"*, *"not clerks, salary at least
975
+ 2000"*. What the region's facets can hold is offered as the first suggestion, **Understood as:**
976
+ *Job: CLERK · Department: SALES · Salary: 1000 – 3000*; choosing it sets those facets and clears the
977
+ search. Values are looked up in the report's own rows (as query parameters). A single value
978
+ (*"clerk"*) stays the ordinary matching suggestion.
953
979
 
954
980
  Everything is a link or a GET form on the report's own URL parameters, so it works without
955
981
  JavaScript, can be bookmarked and is kept in saved reports. A facets panel and smart filters may
@@ -589,6 +589,20 @@ translated label (German nouns). Flash messages are translated where they are se
589
589
  display position, button position, component kind and SQL Workshop tool has its run-time key; and that every
590
590
  language file has exactly the English keys, the same `{placeholders}`, no empty texts and is mostly translated.
591
591
 
592
+ **Texts in the browser** (sprint 123). The builder's own JavaScript (`public/*.js`) asks for texts with keys
593
+ `js.<screen>.<what>`: `shell()` puts every `js.*` key in the developer's language into the page as JSON in an
594
+ attribute, `<div id="builder-texts" hidden data-texts="…">` (escaped like any value; builder pages keep having no
595
+ inline script). A script reads it once and fills placeholders itself:
596
+
597
+ ```js
598
+ const TEXTS = (() => { try { return JSON.parse(document.getElementById('builder-texts')?.dataset.texts || '{}'); } catch { return {}; } })();
599
+ const msg = (key, params = {}) => (TEXTS[key] ?? key).replace(/\{(\w+)\}/g, (m, k) => (k in params ? String(params[k]) : m));
600
+ announce(msg('js.ws.copied', { what }));
601
+ ```
602
+
603
+ The coverage check also scans `public/*.js` for `msg('js.…')` and fails when a key has no English text. Text
604
+ from the server (a JSON answer's `message`) is translated on the server.
605
+
592
606
  Stays English by design: the property editor's field names and help, links to the guide, generated SQL, and pages
593
607
  not moved yet. The builder's language is separate from an application's languages (`src/i18n.ts`).
594
608
 
@@ -23,6 +23,10 @@ The builder's **App Gallery** (`/builder/gallery`) lists what `gallery.json` nam
23
23
  | `sample-grids.sql` | Sample Interactive Grids | an editable grid, select lists, totals, frozen column, row menu, master-detail, views |
24
24
  | `sample-dynamic-actions.sql` | Sample Dynamic Actions | show/hide/enable, set value, refresh, messages, wait popup, classes, inline dialog |
25
25
  | `sample-reporting.sql` | Sample Reporting | interactive report, faceted search, smart filters, a classic report with column options |
26
+ | `sample-maps.sql` | Sample Maps | markers and clusters, layers with GeoJSON and a heat map, a map filtering a list by distance, a location item |
27
+ | `sample-approvals.sql` | Sample Approvals | a workflow started by a form, an approval task and the task list, the workflow console (accounts demo and approver) |
28
+ | `sample-data-loading.sql` | Sample Data Loading | `data_load` processes: by column name and merged, through a data load definition, skipping rows with errors |
29
+ | `sample-files.sql` | Sample Files | a file in the record's columns, several files in a child table, a photo cropped before upload |
26
30
  | `ux-patterns.sql` | UX Pattern Catalog | region templates and options, buttons, items and form layouts, content and page patterns |
27
31
 
28
32
  Each script creates its own schema (`sample_charts`, …), a role (`app_sample_charts`, …) granted to the runtime, the
@@ -16,6 +16,14 @@
16
16
  "description": "Behaviour without JavaScript of your own: show, hide, enable on a condition; values computed on the server; a report refreshed when an item changes; alerts, confirmations, a wait popup, classes, item errors and an inline dialog. Demo account demo / demo."},
17
17
  {"key": "sample-reporting", "title": "Sample Reporting", "alias": "sample-reporting", "icon": "file-text", "file": "samples/sample-reporting.sql",
18
18
  "description": "Reports and the ways to search them: an interactive report with its form, a faceted search, smart filters, and a classic report with column display options, a column template and a link. Demo account demo / demo."},
19
+ {"key": "sample-maps", "title": "Sample Maps", "alias": "sample-maps", "icon": "map", "file": "samples/sample-maps.sql",
20
+ "description": "Maps of Dutch museums without PostGIS: markers with popups and clustering, several layers (a route and an area as GeoJSON, a heat map you switch on), a map that filters the list beside it by distance, and a form with a location item. Demo account demo / demo."},
21
+ {"key": "sample-approvals", "title": "Sample Approvals", "alias": "sample-approvals", "icon": "list-checks", "file": "samples/sample-approvals.sql",
22
+ "description": "Purchase requests that start a workflow: approved by itself below 1,000, otherwise an approval task for the role approver, decided under My tasks; the workflow console with its diagram and history. Demo accounts demo / demo (asks) and approver / approver (decides)."},
23
+ {"key": "sample-data-loading", "title": "Sample Data Loading", "alias": "sample-data-loading", "icon": "file-up", "file": "samples/sample-data-loading.sql",
24
+ "description": "Loading CSV, Excel and JSON files into a table from a page: merged by the key, through a data load definition that maps other headings and a date format, and skipping rows with errors. Example files to download included. Demo account demo / demo."},
25
+ {"key": "sample-files", "title": "Sample Files", "alias": "sample-files", "icon": "files", "file": "samples/sample-files.sql",
26
+ "description": "File upload and download: a file in the record's own column with its name, type, size and date, several files per record in a child table, and a photo cropped square before upload. Demo account demo / demo."},
19
27
  {"key": "ux-patterns", "title": "UX Pattern Catalog", "alias": "ux-patterns", "icon": "palette", "file": "samples/ux-patterns.sql",
20
28
  "description": "The theme's building blocks to copy from: region templates and template options, button styles, item types and form layouts, alerts, badges, colour classes, lists, media lists, timelines, comments, avatars, metric cards, empty states, dialogs, drawers and side columns. Demo account demo / demo."}
21
29
  ],
@@ -0,0 +1,153 @@
1
+ -- =====================================================================
2
+ -- Sample app "Sample Approvals" (App Gallery → Sample Apps)
3
+ --
4
+ -- Approvals, the task list and a workflow on purchase requests:
5
+ -- * a request is made on a form; creating it starts the PURCHASE workflow
6
+ -- * below 1,000 the workflow approves it by itself (an SQL step)
7
+ -- * from 1,000 it asks for approval: a PURCHASE_APPROVAL task for the
8
+ -- users with the role approver, decided under My tasks; the task's
9
+ -- action records the decision on the request
10
+ -- * the console lists the workflows with their diagram and history
11
+ --
12
+ -- Two accounts: demo / demo asks, approver / approver decides (nobody
13
+ -- approves their own request).
14
+ --
15
+ -- Installed by an administrator from the App Gallery, or with
16
+ -- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-approvals.sql
17
+ -- then open /a/sample-approvals and sign in as demo / demo.
18
+ -- =====================================================================
19
+
20
+ create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
21
+ $$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
22
+
23
+ -- 1. Schema and data ---------------------------------------------------------
24
+ create schema sample_approvals;
25
+
26
+ create table sample_approvals.purchase (
27
+ id int generated always as identity primary key,
28
+ title text not null,
29
+ supplier text,
30
+ amount numeric(10, 2) not null check (amount > 0),
31
+ status text not null default 'PENDING' check (status in ('PENDING', 'APPROVED', 'REJECTED')),
32
+ requested_by text,
33
+ requested_at timestamptz not null default now(),
34
+ decided_by text,
35
+ note text
36
+ );
37
+
38
+ -- who asked, and that a new request is pending, come from the session, not the form
39
+ create function sample_approvals.purchase_defaults() returns trigger language plpgsql as $$
40
+ begin
41
+ new.requested_by := coalesce(meta.app_user(), current_user);
42
+ new.status := 'PENDING';
43
+ new.decided_by := null;
44
+ return new;
45
+ end
46
+ $$;
47
+ create trigger purchase_defaults before insert on sample_approvals.purchase
48
+ for each row execute function sample_approvals.purchase_defaults();
49
+
50
+ -- the decision, from the approval task's action (by the approver) or the workflow's SQL step
51
+ -- (which runs as the user who started the workflow, so it says who decided: 'workflow')
52
+ create function sample_approvals.decide(p_id int, p_outcome text, p_note text, p_by text default null) returns void language plpgsql as $$
53
+ begin
54
+ update sample_approvals.purchase
55
+ set status = case lower(p_outcome) when 'approved' then 'APPROVED' when 'rejected' then 'REJECTED' else status end,
56
+ decided_by = coalesce(p_by, meta.app_user()), note = nullif(p_note, '')
57
+ where id = p_id and status = 'PENDING';
58
+ end
59
+ $$;
60
+
61
+ insert into sample_approvals.purchase (title, supplier, amount) values
62
+ ('Printer toner', 'Office Supplies Ltd', 89.50), ('Standing desks (4)', 'ErgoWorks', 2400.00), ('Team lunch', 'De Kantine', 160.00);
63
+ update sample_approvals.purchase set status = 'APPROVED', requested_by = 'demo', decided_by = 'workflow', note = 'Below 1,000: approved automatically' where title in ('Printer toner', 'Team lunch');
64
+ update sample_approvals.purchase set status = 'APPROVED', requested_by = 'demo', decided_by = 'approver', note = 'Within the budget' where title = 'Standing desks (4)';
65
+
66
+ -- 2. The application's database role ----------------------------------------
67
+ do $$ begin
68
+ if not exists (select 1 from pg_roles where rolname = 'app_sample_approvals') then create role app_sample_approvals nologin; end if;
69
+ end $$;
70
+ grant app_sample_approvals to pgkiln_runtime;
71
+ grant usage on schema sample_approvals to app_sample_approvals;
72
+ grant select, insert, update, delete on sample_approvals.purchase to app_sample_approvals;
73
+ grant execute on function sample_approvals.decide(int, text, text, text) to app_sample_approvals;
74
+
75
+ -- 3. The application ----------------------------------------------------------
76
+ insert into meta.app (alias, name, home_page, authentication, db_role)
77
+ values ('sample-approvals', 'Sample Approvals', 1, 'app_users', 'app_sample_approvals');
78
+
79
+ insert into meta.app_user (app_id, username, password_hash, roles)
80
+ select a.id, u.name, meta.hash_password(u.name), u.roles
81
+ from meta.app a, (values ('demo', '{}'::text[]), ('approver', '{approver}'::text[])) as u (name, roles)
82
+ where a.alias = 'sample-approvals';
83
+
84
+ insert into meta.task_definition (app_id, name, type, subject, owner_roles, admin_role, priority, due_in, details_page, details_item, action_code)
85
+ select id, 'PURCHASE_APPROVAL', 'approval', 'Purchase: &TITLE. (&AMOUNT.)', '{approver}'::text[], 'approver', 2, '3 days', 3, 'P3_ID',
86
+ 'select sample_approvals.decide(:DETAIL_PK::int, :OUTCOME, :COMMENT)'
87
+ from meta.app where alias = 'sample-approvals';
88
+
89
+ insert into meta.workflow_definition (app_id, name, title, description, admin_role, steps)
90
+ select id, 'PURCHASE', 'Purchase: &TITLE.', 'A purchase request: approved by itself below 1,000, otherwise by an approver.', 'approver',
91
+ $s$[
92
+ {"name": "AMOUNT_CHECK", "type": "switch", "cases": [{"when": ":AMOUNT::numeric >= 1000", "next": "APPROVE"}], "otherwise": "AUTO_APPROVE"},
93
+ {"name": "AUTO_APPROVE", "type": "sql", "code": "select sample_approvals.decide(:DETAIL_PK::int, 'approved', 'Below 1,000: approved automatically', 'workflow')", "next": "END"},
94
+ {"name": "APPROVE", "type": "task", "task": "PURCHASE_APPROVAL"},
95
+ {"name": "END", "type": "end"}
96
+ ]$s$::jsonb
97
+ from meta.app where alias = 'sample-approvals';
98
+
99
+ insert into meta.page (app_id, page_no, name, title, parent_page)
100
+ select a.id, p.no, p.name, p.name, p.parent
101
+ from meta.app a, (values (1, 'Sample Approvals', null), (4, 'My tasks', 1), (5, 'Workflows', 1)) as p (no, name, parent)
102
+ where a.alias = 'sample-approvals';
103
+
104
+ -- page 2 and its form, page 3: the Report and form wizard on the requests
105
+ select meta.generate_page('sample-approvals', 'report_form', 'sample_approvals.purchase', 2, '{"label": "Purchases", "form_page": 3, "nav": false}');
106
+ update meta.page set parent_page = 1 where app_id = (select id from meta.app where alias = 'sample-approvals') and page_no = 2;
107
+ update meta.region set source = 'select id, title, supplier, amount, status, requested_by, decided_by, note from sample_approvals.purchase order by id desc',
108
+ config = config || '{"formats": {"amount": "999G999G990D00"}}'
109
+ where page_id = pg_temp.page_id('sample-approvals', 2) and type = 'report';
110
+ -- the form asks only what the requester decides; the rest is shown, not edited
111
+ update meta.item set type = 'display'
112
+ where page_id = pg_temp.page_id('sample-approvals', 3) and name in ('P3_STATUS', 'P3_REQUESTED_BY', 'P3_REQUESTED_AT', 'P3_DECIDED_BY', 'P3_NOTE');
113
+
114
+ -- creating a request starts the workflow, after the row is inserted (P3_ID is then known)
115
+ insert into meta.process (page_id, seq, name, type, code, when_button)
116
+ select pg_temp.page_id('sample-approvals', 3), 15, 'Start the approval', 'sql',
117
+ $c$select meta.start_workflow('PURCHASE', :P3_ID, jsonb_build_object('TITLE', :P3_TITLE, 'AMOUNT', :P3_AMOUNT))$c$, 'CREATE';
118
+
119
+ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
120
+ select a.id, n.seq, n.label, n.icon, n.page
121
+ from meta.app a, (values (10, 'Home', 'home', 1), (20, 'Purchases', 'list', 2), (30, 'My tasks', 'check', 4), (40, 'Workflows', 'route', 5)) as n (seq, label, icon, page)
122
+ where a.alias = 'sample-approvals';
123
+
124
+ insert into meta.list (app_id, name, type, description)
125
+ select id, 'APPROVAL_TOPICS', 'static', 'The pages of Sample Approvals' from meta.app where alias = 'sample-approvals';
126
+
127
+ insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
128
+ select a.id, 'APPROVAL_TOPICS', e.seq, e.label, e.icon, e.page, e.description
129
+ from meta.app a, (values
130
+ (10, 'Purchases', 'list', 2, 'Ask for a purchase: creating it starts the PURCHASE workflow.'),
131
+ (20, 'My tasks', 'check', 4, 'Approve or reject what waits for you, and follow what you asked for.'),
132
+ (30, 'Workflows', 'route', 5, 'The workflows you started, with their diagram, current step and history.')
133
+ ) as e (seq, label, icon, page, description)
134
+ where a.alias = 'sample-approvals';
135
+
136
+ insert into meta.region (page_id, seq, title, type, columns, template, source, config)
137
+ select pg_temp.page_id('sample-approvals', r.page), r.seq, r.title, r.type, 12, r.template, r.source, r.config::jsonb
138
+ from (values
139
+ (1, 10, 'Sample Approvals', 'static', 'hero',
140
+ '<p>Signed in as <strong>demo</strong>, ask for a purchase under <em>Purchases</em>. Below 1,000 the workflow approves it at once; from 1,000 it creates an approval task for the role <code>approver</code>. Sign in as <strong>approver</strong> / approver and decide under <em>My tasks</em>: the task''s action writes the decision on the request, and the workflow ends.</p>',
141
+ '{"icon": "check"}'),
142
+ (1, 20, 'Topics', 'list', 'plain', null, '{"list": "APPROVAL_TOPICS", "template": "cards"}'),
143
+ (2, 5, 'About this page', 'static', 'plain',
144
+ '<p class="lead">The form''s Create button runs the wizard''s row process, then <code>select meta.start_workflow(''PURCHASE'', :P3_ID, …)</code>. A trigger fills in who asked and the status from the session, so the form can''t set them.</p>', '{}'),
145
+ (4, 5, 'About this page', 'static', 'plain',
146
+ '<p class="lead">Regions of type <code>tasks</code>: what waits for the signed-in user (as owner, here the role <code>approver</code>), and what they asked for. Approve and Reject run the task definition''s action with <code>:DETAIL_PK</code>, <code>:OUTCOME</code> and <code>:COMMENT</code>.</p>', '{}'),
147
+ (4, 10, 'Waiting for me', 'tasks', 'standard', null, '{"empty": "Nothing waits for you. Sign in as approver to decide on requests of 1,000 or more."}'),
148
+ (4, 20, 'Requested by me', 'tasks', 'collapsible', null, '{"context": "initiated", "completed": true, "empty": "You have not asked for anything that needs approval."}'),
149
+ (5, 5, 'About this page', 'static', 'plain',
150
+ '<p class="lead">A region of type <code>workflows</code>: the workflows the user started, or with <code>"context": "admin"</code> those they administer (here: approver). Open one for its diagram, current step and history; the initiator and administrators can terminate it.</p>', '{}'),
151
+ (5, 10, 'Workflows I started', 'workflows', 'standard', null, '{"completed": true}'),
152
+ (5, 20, 'All purchase workflows (approvers)', 'workflows', 'collapsible', null, '{"context": "admin", "completed": true}')
153
+ ) as r (page, seq, title, type, template, source, config);
@@ -0,0 +1,142 @@
1
+ -- =====================================================================
2
+ -- Sample app "Sample Data Loading" (App Gallery → Sample Apps)
3
+ --
4
+ -- Loading CSV, Excel, JSON and XML files into a table from a page: a load
5
+ -- that matches columns by name and merges by the primary key; a load
6
+ -- through a data load definition that maps other headings, cleans values
7
+ -- and reads a date format; and a load that skips rows with errors and lists
8
+ -- them. The example files are static files of the application.
9
+ --
10
+ -- Installed by an administrator from the App Gallery, or with
11
+ -- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-data-loading.sql
12
+ -- then open /a/sample-data-loading and sign in as demo / demo.
13
+ -- =====================================================================
14
+
15
+ create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
16
+ $$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
17
+
18
+ -- 1. Schema and data ---------------------------------------------------------
19
+ create schema sample_loading;
20
+
21
+ create table sample_loading.product (
22
+ code text primary key check (code ~ '^[A-Z]{2}-\d{3}$'),
23
+ name text not null,
24
+ category text not null check (category in ('TOOLS', 'GARDEN', 'KITCHEN', 'OFFICE')),
25
+ price numeric(8, 2) not null check (price > 0),
26
+ stock int not null default 0 check (stock >= 0),
27
+ added date not null default current_date
28
+ );
29
+
30
+ insert into sample_loading.product (code, name, category, price, stock, added) values
31
+ ('TL-001', 'Hammer', 'TOOLS', 14.95, 40, '2026-01-12'),
32
+ ('TL-002', 'Screwdriver set', 'TOOLS', 19.50, 25, '2026-01-12'),
33
+ ('GD-001', 'Garden hose 20 m', 'GARDEN', 24.99, 12, '2026-03-02'),
34
+ ('KT-001', 'Chef''s knife', 'KITCHEN', 39.00, 8, '2026-04-20');
35
+
36
+ -- 2. The application's database role ----------------------------------------
37
+ do $$ begin
38
+ if not exists (select 1 from pg_roles where rolname = 'app_sample_loading') then create role app_sample_loading nologin; end if;
39
+ end $$;
40
+ grant app_sample_loading to pgkiln_runtime;
41
+ grant usage on schema sample_loading to app_sample_loading;
42
+ grant select, insert, update, delete on sample_loading.product to app_sample_loading;
43
+
44
+ -- 3. The application ----------------------------------------------------------
45
+ insert into meta.app (alias, name, home_page, authentication, db_role)
46
+ values ('sample-data-loading', 'Sample Data Loading', 1, 'app_users', 'app_sample_loading');
47
+
48
+ insert into meta.app_user (app_id, username, password_hash, roles)
49
+ select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias = 'sample-data-loading';
50
+
51
+ -- the example files, served at /a/sample-data-loading/static/<name>
52
+ insert into meta.static_file (app_id, name, mime, content)
53
+ select a.id, f.name, f.mime, convert_to(f.body, 'utf8')
54
+ from meta.app a, (values
55
+ ('products.csv', 'text/csv',
56
+ E'code,name,category,price,stock\nTL-002,Screwdriver set,TOOLS,17.95,30\nTL-003,Spirit level,TOOLS,12.50,15\nOF-001,Stapler,OFFICE,8.95,60\nOF-002,Desk lamp,OFFICE,29.00,10\n'),
57
+ ('products.json', 'application/json',
58
+ E'[{"code": "KT-002", "name": "Cutting board", "category": "KITCHEN", "price": 15.5, "stock": 20},\n {"code": "GD-002", "name": "Pruning shears", "category": "GARDEN", "price": 21.95, "stock": 9}]\n'),
59
+ ('supplier-list.csv', 'text/csv',
60
+ E'Article no.,Description,Group,Unit price,In stock,Since\ngd-003, watering can ,garden,9.99,35,02.05.2026\nkt-003,kitchen scale,Kitchen,18.75,14,15.06.2026\n'),
61
+ ('with-errors.csv', 'text/csv',
62
+ E'code,name,category,price,stock\nOF-003,Notebook,OFFICE,3.50,100\nOF-004,Paper clips,OFFICE,-1,500\nXX,Mystery box,TOYS,10,1\nOF-005,Pencil case,OFFICE,6.25,40\n')
63
+ ) as f (name, mime, body)
64
+ where a.alias = 'sample-data-loading';
65
+
66
+ -- other headings, messy values, a date format: a data load definition (Shared Components → Data load definitions)
67
+ insert into meta.data_load_def (app_id, name, description, table_name, format, mode, columns)
68
+ select a.id, 'SUPPLIER_LIST', 'A supplier''s product list: other headings, codes and groups in lower case, dates as DD.MM.YYYY.',
69
+ 'sample_loading.product', 'csv', 'merge',
70
+ '[{"source": "Article no.", "column": "code", "transform": ["trim", "upper"]},
71
+ {"source": "Description", "column": "name", "transform": ["collapse_spaces", "initcap"]},
72
+ {"source": "Group", "column": "category", "transform": ["trim", "upper"]},
73
+ {"source": "Unit price", "column": "price", "format": "99990.99"},
74
+ {"source": "In stock", "column": "stock", "default": "0"},
75
+ {"source": "Since", "column": "added", "format": "DD.MM.YYYY"}]'
76
+ from meta.app a where a.alias = 'sample-data-loading';
77
+
78
+ insert into meta.page (app_id, page_no, name, title, parent_page)
79
+ select a.id, p.no, p.name, p.name, p.parent
80
+ from meta.app a, (values (1, 'Sample Data Loading', null), (2, 'Products', 1), (3, 'Load a file', 1), (4, 'Load with a definition', 1), (5, 'Skip rows with errors', 1)) as p (no, name, parent)
81
+ where a.alias = 'sample-data-loading';
82
+
83
+ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
84
+ select a.id, n.seq, n.label, n.icon, n.page
85
+ from meta.app a, (values
86
+ (10, 'Home', 'home', 1), (20, 'Products', 'table', 2), (30, 'Load a file', 'upload', 3),
87
+ (40, 'Load with a definition', 'upload', 4), (50, 'Skip rows with errors', 'upload', 5)
88
+ ) as n (seq, label, icon, page)
89
+ where a.alias = 'sample-data-loading';
90
+
91
+ insert into meta.list (app_id, name, type, description)
92
+ select id, 'LOAD_TOPICS', 'static', 'The pages of Sample Data Loading' from meta.app where alias = 'sample-data-loading';
93
+
94
+ insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
95
+ select a.id, 'LOAD_TOPICS', e.seq, e.label, e.icon, e.page, e.description
96
+ from meta.app a, (values
97
+ (10, 'Products', 'table', 2, 'The table the pages load into.'),
98
+ (20, 'Load a file', 'upload', 3, 'CSV, Excel or JSON with the table''s column names, merged by the product code.'),
99
+ (30, 'Load with a definition', 'upload', 4, 'Other headings, values in the wrong case and a date format, mapped by a data load definition.'),
100
+ (40, 'Skip rows with errors', 'upload', 5, 'Load the good rows and list the others, instead of loading nothing.')
101
+ ) as e (seq, label, icon, page, description)
102
+ where a.alias = 'sample-data-loading';
103
+
104
+ insert into meta.region (page_id, seq, title, type, columns, template, source, config, static_id)
105
+ select pg_temp.page_id('sample-data-loading', r.page), r.seq, r.title, r.type, 12, r.template, r.source, r.config::jsonb, r.sid
106
+ from (values
107
+ (1, 10, 'Sample Data Loading', 'static', 'hero',
108
+ '<p>A page with a file item, a button and a <code>data_load</code> process loads a file into a table. It runs as the application''s database role, so grants, constraints, triggers and row level security apply as they do for a form, and by default a file with one bad row loads nothing at all.</p>',
109
+ '{"icon": "upload"}', null),
110
+ (1, 20, 'Topics', 'list', 'plain', null, '{"list": "LOAD_TOPICS", "template": "cards"}', null),
111
+ (2, 10, 'Products', 'report', 'standard',
112
+ 'select code, name, category, price, stock, added from sample_loading.product order by code',
113
+ '{"formats": {"price": "999G990D00"}, "page_size": 50}', null),
114
+ (3, 10, 'Load a file', 'static', 'standard',
115
+ '<p class="lead">The process: <code>{"file_item": "P3_FILE", "table": "sample_loading.product", "mode": "merge"}</code>. Columns are matched by name; a row whose <code>code</code> exists is updated, the others are added. Try <a href="/a/sample-data-loading/static/products.csv" download>products.csv</a> (one update, three new) or <a href="/a/sample-data-loading/static/products.json" download>products.json</a>, then look at <a href="/a/sample-data-loading/2">Products</a>.</p>',
116
+ '{}', 'load'),
117
+ (4, 10, 'Load with a definition', 'static', 'standard',
118
+ '<p class="lead"><a href="/a/sample-data-loading/static/supplier-list.csv" download>supplier-list.csv</a> has other headings (<em>Article no.</em>, <em>Unit price</em>), codes in lower case, extra spaces and dates as 02.05.2026. The data load definition <code>SUPPLIER_LIST</code> maps each heading to a column, trims and upper-cases the code, collapses the spaces in the name and reads the date with <code>DD.MM.YYYY</code>; the process only names it: <code>{"file_item": "P4_FILE", "definition": "SUPPLIER_LIST"}</code>.</p>',
119
+ '{}', 'load'),
120
+ (5, 10, 'Skip rows with errors', 'static', 'standard',
121
+ '<p class="lead"><a href="/a/sample-data-loading/static/with-errors.csv" download>with-errors.csv</a> has a negative price and a code and category the table''s check constraints refuse. With <code>"skip_errors": true</code> the two good rows are loaded and the others are listed with the database''s reason; without it (the other pages) nothing would be loaded.</p>',
122
+ '{}', 'load')
123
+ ) as r (page, seq, title, type, template, source, config, sid);
124
+
125
+ insert into meta.item (page_id, region_id, seq, name, label, type, required, help, config)
126
+ select r.page_id, r.id, 10, 'P' || p.page_no || '_FILE', 'File', 'file', true, 'CSV, Excel (.xlsx) or JSON, at most 2 MB.',
127
+ '{"accept": ".csv,.tsv,.txt,.xlsx,.json", "max_mb": 2, "wide": true}'
128
+ from meta.region r join meta.page p on p.id = r.page_id join meta.app a on a.id = p.app_id
129
+ where a.alias = 'sample-data-loading' and r.static_id = 'load';
130
+
131
+ insert into meta.button (page_id, region_id, seq, name, label, action, hot)
132
+ select r.page_id, r.id, 10, 'LOAD', 'Load', 'submit', true
133
+ from meta.region r join meta.page p on p.id = r.page_id join meta.app a on a.id = p.app_id
134
+ where a.alias = 'sample-data-loading' and r.static_id = 'load';
135
+
136
+ insert into meta.process (page_id, seq, name, type, when_button, success_message, config)
137
+ select pg_temp.page_id('sample-data-loading', x.page), 10, x.name, 'data_load', 'LOAD', x.message, x.config::jsonb
138
+ from (values
139
+ (3, 'Load products', '{inserted} products added, {updated} updated.', '{"file_item": "P3_FILE", "table": "sample_loading.product", "mode": "merge"}'),
140
+ (4, 'Load a supplier list', '{inserted} products added, {updated} updated.', '{"file_item": "P4_FILE", "definition": "SUPPLIER_LIST"}'),
141
+ (5, 'Load the good rows', '{inserted} added, {updated} updated, {failed} skipped.', '{"file_item": "P5_FILE", "table": "sample_loading.product", "mode": "merge", "skip_errors": true}')
142
+ ) as x (page, name, message, config);