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.
- package/SECURITY.md +2 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +30 -3
- package/docs/guide/04-pages-and-regions.md +26 -0
- package/docs/guide/12-development.md +41 -0
- package/examples/gallery/README.md +4 -0
- package/examples/gallery/gallery.json +8 -0
- package/examples/gallery/samples/sample-approvals.sql +153 -0
- package/examples/gallery/samples/sample-data-loading.sql +142 -0
- package/examples/gallery/samples/sample-files.sql +160 -0
- package/examples/gallery/samples/sample-maps.sql +175 -0
- package/examples/gallery/samples/sample-reporting.sql +4 -3
- package/package.json +1 -1
- package/public/builder.css +18 -0
- package/public/worksheet.css +3 -2
- package/src/builder/advisor.ts +1 -1
- package/src/builder/ai-agents.ts +5 -5
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/api.ts +1 -1
- package/src/builder/autorest.ts +2 -2
- package/src/builder/availability.ts +1 -1
- package/src/builder/backups.ts +1 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +4 -4
- package/src/builder/data-security.ts +1 -1
- package/src/builder/dataload.ts +1 -1
- package/src/builder/datapump.ts +1 -1
- package/src/builder/ddl-tools.ts +2 -2
- package/src/builder/designer.ts +134 -124
- package/src/builder/diagnostics.ts +2 -2
- package/src/builder/errors.ts +10 -12
- package/src/builder/feedback.ts +1 -1
- package/src/builder/globalization.ts +1 -1
- package/src/builder/home.ts +59 -57
- package/src/builder/i18n/de.ts +675 -0
- package/src/builder/i18n/en.ts +678 -0
- package/src/builder/i18n/es.ts +675 -0
- package/src/builder/i18n/fr.ts +675 -0
- package/src/builder/i18n/nl.ts +675 -0
- package/src/builder/i18n/pt.ts +675 -0
- package/src/builder/i18n.ts +116 -0
- package/src/builder/language.ts +74 -0
- package/src/builder/launchpad.ts +13 -8
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/plugins.ts +1 -1
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/region-help.ts +1 -1
- package/src/builder/region-settings.ts +3 -1
- package/src/builder/rest-modules.ts +2 -2
- package/src/builder/routes.ts +88 -66
- package/src/builder/sampledata.ts +1 -1
- package/src/builder/scheduling.ts +1 -1
- package/src/builder/scripts.ts +1 -1
- package/src/builder/search.ts +1 -1
- package/src/builder/shared.ts +5 -5
- package/src/builder/sql-charts.ts +2 -2
- package/src/builder/sql-monitor.ts +2 -2
- package/src/builder/sql-tools.ts +6 -0
- package/src/builder/sql-utilities.ts +3 -3
- package/src/builder/sql.ts +24 -22
- package/src/builder/static-files.ts +1 -1
- package/src/builder/subscriptions.ts +1 -1
- package/src/builder/supporting.ts +1 -1
- package/src/builder/table-editor.ts +1 -1
- package/src/builder/team.ts +7 -5
- package/src/builder/themeroller.ts +1 -1
- package/src/builder/top-sql.ts +1 -1
- package/src/builder/ui.ts +87 -77
- package/src/builder/unload.ts +1 -1
- package/src/builder/wizards.ts +1 -1
- package/src/builder/workflow-migrate.ts +1 -1
- package/src/builder/workingcopies.ts +2 -2
- package/src/builder/worksheet.ts +1 -1
- package/src/builder/workspace-home.ts +62 -59
- package/src/builder/workspaces.ts +2 -1
- package/src/i18n/ar.ts +1 -0
- package/src/i18n/cs.ts +1 -0
- package/src/i18n/da.ts +1 -0
- package/src/i18n/de.ts +1 -0
- package/src/i18n/el.ts +1 -0
- package/src/i18n/es.ts +1 -0
- package/src/i18n/fi.ts +1 -0
- package/src/i18n/fr.ts +1 -0
- package/src/i18n/he.ts +1 -0
- package/src/i18n/it.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/nb.ts +1 -0
- package/src/i18n/pl.ts +1 -0
- package/src/i18n/pt.ts +1 -0
- package/src/i18n/ru.ts +1 -0
- package/src/i18n/sv.ts +1 -0
- package/src/i18n/tr.ts +1 -0
- package/src/i18n/uk.ts +1 -0
- package/src/i18n/zh.ts +1 -0
- package/src/i18n.ts +2 -0
- package/src/runtime/ai-filter.ts +22 -6
- package/src/runtime/ask-facets.ts +72 -0
- package/src/runtime/ir-plan.ts +17 -3
- 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) |
|
|
27
|
-
| **Total** | **
|
|
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)).
|
|
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).
|
|
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
|
|
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 |
|
|
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,
|
|
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)) |
|
package/docs/guide/03-builder.md
CHANGED
|
@@ -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
|
|
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
|
|
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);
|