pgkiln 0.54.0 → 0.56.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 (102) hide show
  1. package/SECURITY.md +2 -0
  2. package/db/migrations/127_builder_language.sql +16 -0
  3. package/docs/apex-feature-parity.md +7 -7
  4. package/docs/guide/03-builder.md +30 -3
  5. package/docs/guide/04-pages-and-regions.md +26 -0
  6. package/docs/guide/12-development.md +41 -0
  7. package/examples/gallery/README.md +4 -0
  8. package/examples/gallery/gallery.json +8 -0
  9. package/examples/gallery/samples/sample-approvals.sql +153 -0
  10. package/examples/gallery/samples/sample-data-loading.sql +142 -0
  11. package/examples/gallery/samples/sample-files.sql +160 -0
  12. package/examples/gallery/samples/sample-maps.sql +175 -0
  13. package/examples/gallery/samples/sample-reporting.sql +4 -3
  14. package/package.json +1 -1
  15. package/public/builder.css +18 -0
  16. package/public/worksheet.css +3 -2
  17. package/src/builder/advisor.ts +1 -1
  18. package/src/builder/ai-agents.ts +5 -5
  19. package/src/builder/ai-builder.ts +3 -3
  20. package/src/builder/api.ts +1 -1
  21. package/src/builder/autorest.ts +2 -2
  22. package/src/builder/availability.ts +1 -1
  23. package/src/builder/backups.ts +1 -1
  24. package/src/builder/changelog.ts +1 -1
  25. package/src/builder/data-modeler.ts +4 -4
  26. package/src/builder/data-security.ts +1 -1
  27. package/src/builder/dataload.ts +1 -1
  28. package/src/builder/datapump.ts +1 -1
  29. package/src/builder/ddl-tools.ts +2 -2
  30. package/src/builder/designer.ts +134 -124
  31. package/src/builder/diagnostics.ts +2 -2
  32. package/src/builder/errors.ts +10 -12
  33. package/src/builder/feedback.ts +1 -1
  34. package/src/builder/globalization.ts +1 -1
  35. package/src/builder/home.ts +59 -57
  36. package/src/builder/i18n/de.ts +675 -0
  37. package/src/builder/i18n/en.ts +678 -0
  38. package/src/builder/i18n/es.ts +675 -0
  39. package/src/builder/i18n/fr.ts +675 -0
  40. package/src/builder/i18n/nl.ts +675 -0
  41. package/src/builder/i18n/pt.ts +675 -0
  42. package/src/builder/i18n.ts +116 -0
  43. package/src/builder/language.ts +74 -0
  44. package/src/builder/launchpad.ts +13 -8
  45. package/src/builder/object-browser.ts +1 -1
  46. package/src/builder/plugins.ts +1 -1
  47. package/src/builder/querybuilder.ts +1 -1
  48. package/src/builder/quicksql.ts +1 -1
  49. package/src/builder/region-help.ts +1 -1
  50. package/src/builder/region-settings.ts +3 -1
  51. package/src/builder/rest-modules.ts +2 -2
  52. package/src/builder/routes.ts +88 -66
  53. package/src/builder/sampledata.ts +1 -1
  54. package/src/builder/scheduling.ts +1 -1
  55. package/src/builder/scripts.ts +1 -1
  56. package/src/builder/search.ts +1 -1
  57. package/src/builder/shared.ts +5 -5
  58. package/src/builder/sql-charts.ts +2 -2
  59. package/src/builder/sql-monitor.ts +2 -2
  60. package/src/builder/sql-tools.ts +6 -0
  61. package/src/builder/sql-utilities.ts +3 -3
  62. package/src/builder/sql.ts +24 -22
  63. package/src/builder/static-files.ts +1 -1
  64. package/src/builder/subscriptions.ts +1 -1
  65. package/src/builder/supporting.ts +1 -1
  66. package/src/builder/table-editor.ts +1 -1
  67. package/src/builder/team.ts +7 -5
  68. package/src/builder/themeroller.ts +1 -1
  69. package/src/builder/top-sql.ts +1 -1
  70. package/src/builder/ui.ts +87 -77
  71. package/src/builder/unload.ts +1 -1
  72. package/src/builder/wizards.ts +1 -1
  73. package/src/builder/workflow-migrate.ts +1 -1
  74. package/src/builder/workingcopies.ts +2 -2
  75. package/src/builder/worksheet.ts +1 -1
  76. package/src/builder/workspace-home.ts +62 -59
  77. package/src/builder/workspaces.ts +2 -1
  78. package/src/i18n/ar.ts +1 -0
  79. package/src/i18n/cs.ts +1 -0
  80. package/src/i18n/da.ts +1 -0
  81. package/src/i18n/de.ts +1 -0
  82. package/src/i18n/el.ts +1 -0
  83. package/src/i18n/es.ts +1 -0
  84. package/src/i18n/fi.ts +1 -0
  85. package/src/i18n/fr.ts +1 -0
  86. package/src/i18n/he.ts +1 -0
  87. package/src/i18n/it.ts +1 -0
  88. package/src/i18n/ja.ts +1 -0
  89. package/src/i18n/ko.ts +1 -0
  90. package/src/i18n/nb.ts +1 -0
  91. package/src/i18n/pl.ts +1 -0
  92. package/src/i18n/pt.ts +1 -0
  93. package/src/i18n/ru.ts +1 -0
  94. package/src/i18n/sv.ts +1 -0
  95. package/src/i18n/tr.ts +1 -0
  96. package/src/i18n/uk.ts +1 -0
  97. package/src/i18n/zh.ts +1 -0
  98. package/src/i18n.ts +2 -0
  99. package/src/runtime/ai-filter.ts +22 -6
  100. package/src/runtime/ask-facets.ts +72 -0
  101. package/src/runtime/ir-plan.ts +17 -3
  102. package/src/runtime/smart-filters.ts +36 -2
package/SECURITY.md CHANGED
@@ -109,6 +109,8 @@ first version (MVP). Every finding below has a regression test in
109
109
  | Mail queue, workspace mail and network access (sprint 118) | Administration → Mail queue: administrators see every workspace, a workspace's administrators only their current workspace's applications, other developers get 403; Retry and Purge (one mail or all failed) check the mail's workspace again on the server and need the CSRF token; bodies and attachments are never shown. A workspace's own mail service is managed by its administrators only; its secret is encrypted like a web credential's, write-only, never shown or exported (not in the workspace export); it has no API base URL or token URL, and its SMTP host must pass the installation's allow-list and the workspace's network access list and resolve to a public address (unless `PGKILN_REST_PRIVATE_HOSTS` lists it), resolved once and connected to by that address (no DNS rebinding). A workspace's network access list narrows `PGKILN_REST_ALLOWED_HOSTS` on the server for every outgoing web call of its applications (`meta.web_request`, REST data sources incl. write-back and the response cache, invoke API, token endpoints, object storage, the builder's Test, REST source catalogs), on every redirect hop; it can't widen the installation's list or the private-address checks. Refusals go to the debug log and `meta.network_refusal` (7 days). Also fixed: *REST source catalogs → read from a URL* fetched the URL before checking the developer's session (any visitor could make the server call an allowed host); it is now read only after the developer and CSRF checks. |
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
+ | 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. |
112
114
 
113
115
  ## Findings of the MVP review and their fixes
114
116
 
@@ -0,0 +1,16 @@
1
+ -- ---------------------------------------------------------------------
2
+ -- 127: the builder in more languages (sprint 121, WS-7; APEX: the
3
+ -- builder's language links on the sign-in page and the workspace home).
4
+ --
5
+ -- meta.developer.language: the builder language each developer chose (the
6
+ -- language menu in the builder's status bar, or a language link on the
7
+ -- builder's sign-in page); null = English. The builder's own texts are
8
+ -- message files in src/builder/i18n/ (en, de, es, fr, nl, pt); the check
9
+ -- keeps the column to codes of that shape, the server accepts only the
10
+ -- languages it has. A developer account is installation data: not
11
+ -- exported with an application or a workspace.
12
+ -- ---------------------------------------------------------------------
13
+ alter table meta.developer
14
+ add column language text check (language ~ '^[a-z]{2}(-[A-Z]{2})?$');
15
+ comment on column meta.developer.language is
16
+ 'The developer''s builder language (127): en, de, es, fr, nl or pt; null = English.';
@@ -23,8 +23,8 @@ Last reviewed: 2026-10-20 against **APEX 26.2** (sprint 120: the App Gallery's s
23
23
  | Data and integration | 13 | 0 | 0 | 2 | REST source catalogs from OpenAPI documents; Generate DDL and schema comparison; `meta.data_export` and data export downloads (CSV, JSON, XML, HTML, Excel, PDF); e-mail through SMTP (password or OAuth2), Microsoft Graph, Amazon SES and HTTP APIs; REST APIs via PostgREST, REST data sources that write back and synchronise, web credentials with OAuth2 grants, CSV/XLSX/JSON/XML loading with saved definitions and unloading, SQL scripts and Quick SQL, report PDFs and document templates |
24
24
  | Workflow, automation and AI | 6 | 0 | 0 | 0 | Scheduled automations with several actions and runs from SQL, approvals, a task list and workflows with parallel branches, versions and invoke-API steps; AI with Claude or OpenAI: *Generate text with AI*, an assistant region with tools, natural-language report filters and blueprints |
25
25
  | Administration | 14 | 0 | 0 | 0 | An APEX-like sign-in with a workspace and a Workspace home; a mail queue dashboard, a workspace's own mail service and network access list (26.1 / 26.2); a workspace's components exported and imported as one file; workspace message, environment banner and utilization; isolated workspaces with their own database logins and roles, and workspace administrators; application availability and Run only, automatic backups, user feedback; a developer toolbar with a session state viewer and debug per session; Top SQL per app; debug messages per request; an install/upgrade log |
26
- | APEX 26.2 workspace (WS-1 … WS-13) | 12 | 0 | 1 | 1 | Workspace home, Data Reporter as a workspace tool, Team Development, AI Agents, AI interactive reports, Workflow 26.2, the mail queue, workspace mail and network access, the 26.1 component details, the App Builder list's details, the Pillar navigation appearances and data security per table (rows, columns and masked values), the App Gallery's single-topic sample apps, UX Pattern Catalog and Utility Apps; not yet: the builder in more languages |
27
- | **Total** | **160** | **0** | **1** | **4** | 165 APEX features compared: 97% available, one missing (the open 26.2 workspace item WS-7) |
26
+ | APEX 26.2 workspace (WS-1 … WS-13) | 13 | 0 | 0 | 1 | Workspace home, Data Reporter as a workspace tool, Team Development, AI Agents, AI interactive reports, Workflow 26.2, the mail queue, workspace mail and network access, the 26.1 component details, the App Builder list's details, the Pillar navigation appearances and data security per table (rows, columns and masked values), the App Gallery's single-topic sample apps, UX Pattern Catalog and Utility Apps; the builder in six languages |
27
+ | **Total** | **161** | **0** | **0** | **4** | 165 APEX features compared: 98% available, none missing, 4 deliberately not planned |
28
28
 
29
29
  (Counts are of the rows in the tables below. A review on 2026-10-06 added 18 rows for APEX features that were
30
30
  not compared before, one on 2026-10-12 added two more (property help, application availability), and one on
@@ -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
 
@@ -221,7 +221,7 @@ the 26.2 comparison.)
221
221
 
222
222
  | APEX | pgkiln | Notes |
223
223
  |---|---|---|
224
- | Developer accounts, workspace sign-in | ✅ | (sprint 61) The builder's sign-in page like APEX's: workspace (optional; it becomes the current one, a wrong one answers like a wrong password), username, password with a Show button, *Remember workspace and username*, a link to Administration. (sprint 68) **Find my workspace**: developers' e-mail addresses, and a mail with their workspaces through the builder's mail service (the same answer for every address, limited per address and IP). **Missing:** the sign-in page's language links (the builder is in English only) |
224
+ | Developer accounts, workspace sign-in | ✅ | (sprint 61) The builder's sign-in page like APEX's: workspace (optional; it becomes the current one, a wrong one answers like a wrong password), username, password with a Show button, *Remember workspace and username*, a link to Administration. (sprint 68) **Find my workspace**: developers' e-mail addresses, and a mail with their workspaces through the builder's mail service (the same answer for every address, limited per address and IP). (sprint 121) The sign-in page's language links: Deutsch, English, Español, Français, Nederlands, Português (WS-7) |
225
225
  | Workspace home (tool cards, top apps and users, summary, workspace and system message) | ✅ | (sprint 109) `/builder/home` opens after sign-in ([chapter 3](guide/03-builder.md#workspace-home)): App Builder, SQL Workshop, Data Reporter and App Gallery cards; the workspace message edited in place; a Summary of applications, tables, developers and reporting applications; top applications, users and developers over an hour, a day or a week; a system message with the server's version and, with the optional release check (`PGKILN_RELEASE_CHECK`, off by default), *pgkiln X is available*. The App Builder moved to `/builder/apps`; `/builder` opens either, per workspace. |
226
226
  | Workspace message, environment banner, workspace utilization, preferences and extension links (Manage Service) | ✅ | (sprint 61) An **Administration** menu in the icon rail; administrators and workspace administrators set a **workspace message** (on the App Builder home) and an **environment banner** (a coloured label on every builder page of the workspace), and see **workspace utilization** (applications, pages, page views, users, static files, schema storage) ([chapter 3](guide/03-builder.md#workspace-message-and-environment-banner)). (sprint 62) **Workspace preferences**: the SQL Workshop and the applications' RESTful services (REST modules) on or off per workspace; **extension links** in an Extensions menu of the rail ([chapter 3](guide/03-builder.md#workspace-preferences)). **Missing:** the account login controls of APEX's preferences (pgkiln's password policy and sign-in throttling are installation-wide) |
227
227
  | Instance administration, install/upgrade logs (26.1) | ✅ | Versioned migrations (`npm run db:migrate`), every run logged; Workspace utilities → **Installation** (administrators): server version, install and upgrade runs with errors, applied migrations, and warnings for migrations the database misses or the server doesn't know ([chapter 3](guide/03-builder.md)). Workspace utilities → **Instance settings**: session length and sign-in throttling (stored in the database, over the environment variables) and an overview of the server's configuration without secrets; a server whose database lacks migrations says so (503) or applies them (`MIGRATE_ON_START`). Other settings stay environment variables |
@@ -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 | ❌ | Applications have twenty-two built-in languages; the builder and its sign-in page are English only |
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 |
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)) |
@@ -237,7 +237,7 @@ Every builder page has the same frame, modelled on an IDE:
237
237
  API, Settings, Search and Advisor, with Export and Run on the right.
238
238
 
239
239
  - **The status bar** at the bottom: who is signed in, the current [workspace](#workspaces), the database, the
240
- builder's language and the pgkiln version.
240
+ [builder language](#choosing-the-builder-language) (a menu) and the pgkiln version.
241
241
  - **An environment banner** (when the current workspace has one) across the top: a coloured band with a label such as
242
242
  DEVELOPMENT or TEST, so nobody mistakes one environment for another.
243
243
 
@@ -257,6 +257,27 @@ application of a workspace you are not a member of) shows a **Page not found** p
257
257
  theme with a link to the App Builder home; a server error shows **Something went wrong** (the error
258
258
  is in the server log, not on the page). Scripts get the plain answers they always got.
259
259
 
260
+ ### Choosing the builder language
261
+
262
+ (sprint 121, APEX: the builder's language links) The builder speaks **Deutsch, English, Español, Français, Nederlands
263
+ and Português** (Brazilian). English is the default.
264
+
265
+ - **On the sign-in page** the languages are links under the form: choosing one shows the sign-in page in that
266
+ language and, when you then sign in, stores it as your builder language. The choice is remembered on this device
267
+ (a cookie), so the sign-in page keeps it after you sign out.
268
+ - **When signed in**, the language in the status bar at the bottom is a menu: pick another language and the page
269
+ you are on comes back in it.
270
+
271
+ The language is stored **per developer** (in your developer account), so it follows you to every session and every
272
+ device; other developers keep their own. It only changes the builder: your applications have their own languages
273
+ (chapter 7).
274
+
275
+ What is translated: the builder's frame (the rail, the top bar and its search, your account menu, the status bar,
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
279
+ Page Designer's property names and their help, the links to this guide, and any SQL the builder generates.
280
+
260
281
  ## Workspace home
261
282
 
262
283
  (sprint 109; APEX: the workspace home page) The page the builder opens after signing in, `/builder/home`, above the
@@ -346,8 +367,14 @@ server, `?q=`); every word must be in the title or the description.
346
367
  four views, create on click, drag and drop through a function), **Sample Interactive Grids** (an editable grid,
347
368
  select lists, totals, a frozen column, a row menu, master-detail, the grid's views), **Sample Dynamic Actions**
348
369
  (show and hide, values from the server, refresh, messages, a wait popup, classes, an inline dialog) and
349
- **Sample Reporting** (interactive report, faceted search, smart filters, a classic report with column options and
350
- a column template);
370
+ **Sample Reporting** (interactive report, faceted search, smart filters that also read a phrase, a classic report
371
+ with column options and a column template); and (sprint 122; APEX: Sample Maps, Sample Approvals, Sample Data
372
+ Loading, Sample File Upload and Download) **Sample Maps** (markers and clusters, layers with GeoJSON and a heat
373
+ map, a map that filters a list by distance, a location item, without PostGIS), **Sample Approvals** (a form
374
+ starts a workflow: approved by itself below 1,000, otherwise an approval task decided under My tasks; the
375
+ workflow console; accounts **demo** and **approver**), **Sample Data Loading** (`data_load` processes by column
376
+ name, through a data load definition and skipping bad rows, with example files to download) and **Sample Files**
377
+ (a file in the record's columns, several files in a child table, a photo cropped before upload);
351
378
  - the **UX Pattern Catalog** (APEX: the Universal Theme app): region templates and template options, button
352
379
  styles, item types and form layouts, alerts, badges, colour classes, lists, media lists, timelines, comments,
353
380
  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
@@ -263,6 +263,11 @@ src/
263
263
  builder/
264
264
  components.ts property spec of every component (drives the property editor)
265
265
  ui.ts IDE shell (icon rail, toolbar, breadcrumb, status bar), builder theme, form helpers, CSRF check, app tabs
266
+ i18n.ts (sprint 121) the builder's own texts: BUILDER_LANGUAGES, tr(s) / translatorFor(lang) → tx(key, params),
267
+ tx.rich (markup in a sentence), tx.dyn (keys made at run time), tx.label (known breadcrumb labels)
268
+ i18n/ en.ts defines the builder's keys; de, es, fr, nl, pt are Records of the same keys
269
+ language.ts (migration 127) the status bar's language menu (POST /builder/language), the sign-in page's
270
+ language links (?lang=, a cookie), meta.developer.language
266
271
  routes.ts sign-in, app home, settings, activity, developers, create (POST, via newapp.ts)/import (POST)
267
272
  wizards.ts create page wizards: step 2 forms per page type (defaults from meta.wizard_defaults), POST → meta.generate_page
268
273
  (the generators are PL/pgSQL in migration 047: catalog, defaults, form/cards/calendar/chart/map/facets/master-detail)
@@ -484,6 +489,8 @@ test/
484
489
  logic.test.ts computations, branches, menu buttons and badges, new dynamic actions, build options, export
485
490
  page-logic.test.ts download, chain (background jobs) and workflow processes, function/app branches, dialog_closed (HR page 28)
486
491
  code-editor.test.ts code editor: completions scoped to the app's role, the check, marked fields
492
+ builder-i18n.test.ts (sprint 121) builder languages: the coverage check of src/builder against i18n/en.ts, the message
493
+ files, Deutsch on the sign-in page, the status bar menu, the choice per developer, English fallback
487
494
  builder-home.test.ts App Builder home: search, sort, views, Recent, Create/Import pages, dashboard, utilities
488
495
  app-list.test.ts (116) the App Builder list's Updated by, working copies and lock; what counts as a change
489
496
  import-only.test.ts (116) pgkiln import --replace --only: a page or component file, refusals
@@ -531,6 +538,7 @@ test/
531
538
  ai-mock.ts a local mock of the Claude Messages API (SSE stream) and OpenAI Chat Completions
532
539
  helpers.ts a cookie-keeping test browser
533
540
  e2e/responsive.test.ts browser tests at phone/tablet/desktop widths (Playwright)
541
+ e2e/builder-i18n.test.ts (sprint 121) Deutsch on the sign-in page, the status bar's language menu, German screens at 390 px, CSP
534
542
  e2e/gallery.test.ts (sprint 120) the gallery's sample apps: every page at 4 widths, CSP, axe; dynamic actions; the four tabs at 390 px
535
543
  e2e/admin-mail-network.test.ts (sprint 118) Mail queue, Network access and a workspace's mail service at 390 px without JavaScript
536
544
  e2e/pillar.test.ts (sprint 117) Pillar appearances: axe in light and dark, subtle colours, current vs parent entry, Arabic RTL, 390 px, preview
@@ -551,6 +559,39 @@ test/
551
559
  e2e/ai.test.ts Generate text with AI on HR page 37 in a browser (mock Claude): dynamic actions without a submit, errors, no JavaScript
552
560
  ```
553
561
 
562
+ ## Builder texts in more languages
563
+
564
+ (sprint 121) The builder's own texts are keyed messages, like the runtime's: `src/builder/i18n/en.ts` defines every
565
+ key (`<screen>.<what>`, e.g. `rail.app_builder`, `pd.menu.create`, `ab.count` with `{n}`), and `de.ts`, `es.ts`,
566
+ `fr.ts`, `nl.ts` and `pt.ts` are `Record<BuilderKey, string>` of the same keys. A page takes its translator from the
567
+ session and uses it for every text it shows:
568
+
569
+ ```ts
570
+ const tx = tr(s); // the developer's language (meta.developer.language, in the session)
571
+ html`<h2>${tx('wh.summary')}</h2><p>${tx('ab.count', { n: 3 })}</p>`; // plain text: html`` escapes it
572
+ tx.rich('ab.none_yet', { create: html`<a href="…">${tx('ab.create_one')}</a>` }); // markup inside a sentence
573
+ tx.dyn(`gallery.region.${type}`, REGION_LABELS[type][0]); // a key made at run time, with a fallback
574
+ ```
575
+
576
+ A missing text falls back to English, never to the raw key. Shared helpers that render chrome take the session as
577
+ an optional last argument (`appHeader(a, 'pages', s)`, `workshopTabs('sql', s)`); `shell()` translates known English
578
+ breadcrumb labels (`tx.label`) so pages that aren't moved yet still get a translated trail.
579
+
580
+ **Moving a screen over**: replace its literal texts with `tx('key')`, add each key with its English text to `en.ts`
581
+ (keep the English exactly as it was: tests and e2e tests assert it), then add the key to the five other files
582
+ (TypeScript refuses a file with a missing or an unknown key). A sentence with a link is one key with a `{link}`
583
+ placeholder and `tx.rich`, not pieces glued together; a count has a `_one` and a plural key; never lower-case a
584
+ translated label (German nouns). Flash messages are translated where they are set.
585
+
586
+ **The coverage check** (`test/builder-i18n.test.ts`, part of `npm test` and CI) scans `src/builder/**/*.ts` for
587
+ `tx('…')`, `tx.rich('…')` and `tr(s)('…')` and fails when a key has no English text (templated keys such as
588
+ `` `ago.${unit}` `` must match at least one English key); checks that every region type, item type, button action,
589
+ display position, button position, component kind and SQL Workshop tool has its run-time key; and that every
590
+ language file has exactly the English keys, the same `{placeholders}`, no empty texts and is mostly translated.
591
+
592
+ Stays English by design: the property editor's field names and help, links to the guide, generated SQL, and pages
593
+ not moved yet. The builder's language is separate from an application's languages (`src/i18n.ts`).
594
+
554
595
  ## Principles
555
596
 
556
597
  - **User-facing texts go through the translator**: `ctx.locale.t('key')` for pgkiln's own texts (add the key to `en` and `nl` in `src/i18n.ts` and to `src/i18n/de.ts`, `fr.ts`, `es.ts`; TypeScript checks that every language has every key), `ctx.locale.tr(text)` for texts derived from application metadata.
@@ -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);