pgkiln 0.31.0 → 0.35.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/README.md +2 -2
- package/SECURITY.md +10 -0
- package/db/migrations/076_form_lost_update.sql +50 -0
- package/db/migrations/077_warn_unsaved.sql +41 -0
- package/db/migrations/078_url_region.sql +12 -0
- package/db/migrations/079_developer_toolbar.sql +53 -0
- package/db/migrations/080_search.sql +326 -0
- package/db/migrations/081_mail.sql +621 -0
- package/db/migrations/082_app_backups.sql +73 -0
- package/db/migrations/083_availability.sql +156 -0
- package/db/migrations/084_feedback.sql +46 -0
- package/db/migrations/085_page_behaviour.sql +65 -0
- package/db/migrations/086_data_settings.sql +599 -0
- package/docs/apex-feature-parity.md +55 -35
- package/docs/guide/01-installation.md +1 -0
- package/docs/guide/03-builder.md +118 -2
- package/docs/guide/04-pages-and-regions.md +115 -0
- package/docs/guide/05-items.md +47 -0
- package/docs/guide/06-processing.md +210 -2
- package/docs/guide/07-dynamic-actions.md +60 -2
- package/docs/guide/08-security.md +13 -2
- package/docs/guide/09-reference.md +21 -0
- package/docs/guide/11-from-apex.md +5 -1
- package/docs/guide/12-development.md +33 -1
- package/docs/guide/14-globalization.md +15 -0
- package/docs/guide/15-extensions.md +1 -1
- package/docs/guide/18-cli.md +30 -1
- package/docs/guide/20-ai-agents.md +3 -2
- package/examples/hr/hr_50_search.sql +47 -0
- package/examples/hr/hr_51_mail.sql +70 -0
- package/examples/hr/hr_52_feedback.sql +15 -0
- package/examples/hr/hr_53_data_settings.sql +108 -0
- package/package.json +3 -1
- package/public/app.css +62 -0
- package/public/app.js +408 -11
- package/public/builder.css +11 -0
- package/public/builder.js +27 -0
- package/src/app.ts +8 -0
- package/src/appfiles.ts +12 -0
- package/src/automations.ts +12 -0
- package/src/builder/advisor.ts +54 -14
- package/src/builder/arrange.ts +2 -2
- package/src/builder/availability.ts +74 -0
- package/src/builder/backups.ts +134 -0
- package/src/builder/components.ts +109 -15
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +82 -0
- package/src/builder/forms.ts +4 -1
- package/src/builder/home.ts +3 -0
- package/src/builder/locks.ts +15 -5
- package/src/builder/mail.ts +425 -0
- package/src/builder/newapp.ts +1 -1
- package/src/builder/region-help.ts +71 -0
- package/src/builder/routes.ts +44 -1
- package/src/builder/search.ts +4 -4
- package/src/builder/shared.ts +3 -2
- package/src/builder/ui.ts +7 -1
- package/src/cli/apps.ts +46 -5
- package/src/cli/main.ts +21 -5
- package/src/cli/mcp.ts +16 -4
- package/src/cli/replace.ts +36 -4
- package/src/geocode.ts +44 -0
- package/src/i18n/ar.ts +39 -0
- package/src/i18n/cs.ts +39 -0
- package/src/i18n/da.ts +39 -0
- package/src/i18n/de.ts +39 -0
- package/src/i18n/el.ts +39 -0
- package/src/i18n/es.ts +39 -0
- package/src/i18n/fi.ts +39 -0
- package/src/i18n/fr.ts +39 -0
- package/src/i18n/he.ts +39 -0
- package/src/i18n/it.ts +39 -0
- package/src/i18n/ja.ts +39 -0
- package/src/i18n/ko.ts +39 -0
- package/src/i18n/nb.ts +39 -0
- package/src/i18n/pl.ts +39 -0
- package/src/i18n/pt.ts +39 -0
- package/src/i18n/ru.ts +39 -0
- package/src/i18n/sv.ts +39 -0
- package/src/i18n/tr.ts +39 -0
- package/src/i18n/uk.ts +39 -0
- package/src/i18n/zh.ts +39 -0
- package/src/i18n.ts +78 -0
- package/src/mail/mail.ts +148 -0
- package/src/mail/providers.ts +444 -0
- package/src/metadata.ts +26 -8
- package/src/runtime/ajax.ts +97 -0
- package/src/runtime/authz.ts +1 -1
- package/src/runtime/availability.ts +37 -0
- package/src/runtime/context.ts +46 -6
- package/src/runtime/data-export.ts +94 -0
- package/src/runtime/data-items.ts +194 -0
- package/src/runtime/devbar.ts +182 -0
- package/src/runtime/engine.ts +26 -1
- package/src/runtime/feedback.ts +128 -0
- package/src/runtime/items.ts +8 -0
- package/src/runtime/processes.ts +83 -2
- package/src/runtime/regions.ts +8 -0
- package/src/runtime/render.ts +33 -7
- package/src/runtime/rest.ts +4 -0
- package/src/runtime/routes.ts +59 -5
- package/src/runtime/search.ts +252 -0
- package/src/runtime/url-region.ts +93 -0
- package/src/security.ts +11 -1
- package/src/server.ts +2 -0
- package/src/session.ts +8 -2
- package/src/workingcopy.ts +2 -2
|
@@ -13,6 +13,25 @@ A page has a number (unique in the app), a name, a title, and these behaviours:
|
|
|
13
13
|
| `requires_auth` | boolean | `false` makes the page public in an app with a login |
|
|
14
14
|
| `authz` | scheme name | Who may open it (403 otherwise) |
|
|
15
15
|
| `protection` | `checksum`, `unrestricted` | Whether URL item values need a checksum |
|
|
16
|
+
| `warn_unsaved` | boolean | Ask before the user leaves the page with unsaved changes, see [below](#warn-on-unsaved-changes) |
|
|
17
|
+
|
|
18
|
+
### Warn on unsaved changes
|
|
19
|
+
|
|
20
|
+
With **Warn on unsaved changes** (Page Designer → Page → Appearance; APEX: the page attribute of the
|
|
21
|
+
same name), the browser asks *"You have unsaved changes. Leave this page anyway?"* when the user
|
|
22
|
+
leaves the page while an item has a value other than the one the page was shown with: a link, a
|
|
23
|
+
redirect button (such as *Cancel*), the navigation menu, closing a dialog (its × or Escape),
|
|
24
|
+
reloading or closing the tab. Submitting the page with a button doesn't ask (that saves). After a
|
|
25
|
+
submit that failed (a validation error), the values on the page are not saved yet, so leaving asks
|
|
26
|
+
even without a further change. The check is in the browser (`pgkiln.isChanged()` tells your own
|
|
27
|
+
JavaScript, like APEX's `apex.page.isChanged()`); without JavaScript there is none.
|
|
28
|
+
|
|
29
|
+
- **Leave an item out** with `{"warn_unsaved": false}` in its attributes (APEX: the item's *Warn on
|
|
30
|
+
Unsaved Changes: Ignore*), for example a search field.
|
|
31
|
+
- The interactive grid has its own check of unsaved rows, which is always on.
|
|
32
|
+
- A value a dynamic action changes on page load counts as a change.
|
|
33
|
+
- The create page wizards switch it on for the form pages they make; existing pages have it off
|
|
34
|
+
until you switch it on.
|
|
16
35
|
|
|
17
36
|
### Dialogs and drawers
|
|
18
37
|
|
|
@@ -113,6 +132,8 @@ database only checks the shape (at most 12 names of lower case letters, digits a
|
|
|
113
132
|
| [`template_component`](#template-components) | Each row (or all rows) of a SELECT through a template component: badges, contact cards, timelines, your own |
|
|
114
133
|
| [`tasks`](06-processing.md#approvals-and-the-task-list) | Task list: approvals and actions for the signed-in user |
|
|
115
134
|
| [`workflows`](06-processing.md#workflows) | Workflow console: the workflows the user started or administers |
|
|
135
|
+
| [`search`](#search-search-region) | One search field over several sources (search configurations), results grouped per source |
|
|
136
|
+
| [`url`](#url-url-region) | Another site's page (or a page of this server) in a sandboxed frame |
|
|
116
137
|
| [`static`](#static-and-dynamic-content) | Fixed HTML with `&ITEM.` substitutions |
|
|
117
138
|
| [`dynamic`](#static-and-dynamic-content) | HTML produced by a SELECT |
|
|
118
139
|
|
|
@@ -411,6 +432,18 @@ that column.
|
|
|
411
432
|
Typical form buttons (as generated by the wizard): *Cancel* (redirect), *Delete* (condition
|
|
412
433
|
`:P3_ID is not null`), *Apply Changes* (same condition) and *Create* (condition `:P3_ID is null`).
|
|
413
434
|
|
|
435
|
+
**Lost updates** (APEX: *Prevent Lost Updates* and *Lock Row*). When the form fetches its row, the
|
|
436
|
+
page also gets a checksum of the row's form columns (file columns aside). On *Save* or *Delete*,
|
|
437
|
+
`form_dml` locks the row (`SELECT … FOR UPDATE`), computes the checksum again and refuses the
|
|
438
|
+
change when it differs: *"Someone else changed this record after you opened it. Reload the page to
|
|
439
|
+
see the current values, then make your changes again."* Nothing is written, and the other user's
|
|
440
|
+
change is kept. Columns that are not on the form don't count, and the checksum doesn't depend on
|
|
441
|
+
the user's time zone or date format. The check is on by default; switch it off for one form with
|
|
442
|
+
`{"lost_update": false}` in the region's attributes. The checksum is `meta.row_version(table,
|
|
443
|
+
key_column, key, columns)`, which runs as the application's role (so its grants and row level
|
|
444
|
+
security apply). A process that changes the same row *before* `form_dml` in the same submit makes
|
|
445
|
+
the check fail: change the row after `form_dml`, or switch the check off.
|
|
446
|
+
|
|
414
447
|
The easiest way to get a form is the **Report and form** wizard, which opens the form as a modal
|
|
415
448
|
dialog from the report, or the **Form** wizard for a form page on its own (see
|
|
416
449
|
[Create pages from a table](03-builder.md#create-pages-from-a-table-wizards)). The Cards, Calendar,
|
|
@@ -1264,6 +1297,88 @@ first rows and a *Show all* button), `char-counter` (item: a maximum length with
|
|
|
1264
1297
|
an event in `pgkiln_plugins.event_log`). HR example part 47 installs them and uses all four on page
|
|
1265
1298
|
40 (*Plug-ins*).
|
|
1266
1299
|
|
|
1300
|
+
### `search` (search region)
|
|
1301
|
+
|
|
1302
|
+
One search field over several sources, with the results grouped per source (APEX 22.2: the Search
|
|
1303
|
+
region and search configurations). Each source is a **search configuration** (Shared Components →
|
|
1304
|
+
Search configurations):
|
|
1305
|
+
|
|
1306
|
+
| Property | Meaning |
|
|
1307
|
+
|---|---|
|
|
1308
|
+
| Name | The static ID the region names, e.g. `CUSTOMERS` |
|
|
1309
|
+
| Label | The heading of its results (translated like the page's texts) |
|
|
1310
|
+
| Sequence | The order of the groups |
|
|
1311
|
+
| Source type, source | `table`: a table or view (`sales.customers`) · `sql`: a SELECT, `:ITEM` binds allowed · `list`: the entries of a [list](#list-lists) the user may see (their label and description are searched, they link where the entry links) |
|
|
1312
|
+
| Searchable columns | The columns the search text is looked for in. Empty: the title, subtitle and description columns |
|
|
1313
|
+
| Title, subtitle, description, badge column | What each result shows. The title is required (table, sql), and results are ordered by it |
|
|
1314
|
+
| Icon | An [icon name](09-reference.md#icons), or `#COLUMN#` for a column that holds icon names |
|
|
1315
|
+
| Link | Where a result goes: `{"page": 3, "items": {"P3_ID": "#ID#"}}` (`#COLUMN#` is the row's value; the link gets a checksum, a modal page opens as a dialog) |
|
|
1316
|
+
| Maximum results | Per configuration, 1–100 (default 10); "Showing the first *n* rows." when there are more |
|
|
1317
|
+
|
|
1318
|
+
**How it searches.** Each word of the search text must be found (case-insensitively, anywhere in
|
|
1319
|
+
the value) in one of the searchable columns, so `clerk mil` finds MILLER the clerk. `%` and `_` are
|
|
1320
|
+
plain characters. The text is a query parameter, never SQL; the queries run as the application's
|
|
1321
|
+
role (its grants and row level security apply), each on its own, so a configuration that fails
|
|
1322
|
+
shows its error and the others still answer. The found words are marked in the results.
|
|
1323
|
+
|
|
1324
|
+
```sql
|
|
1325
|
+
-- a configuration on a query: employees with their department
|
|
1326
|
+
select e.empno, initcap(e.ename) as name, initcap(e.job) as job, d.dname as department
|
|
1327
|
+
from hr.emp e left join hr.dept d on d.deptno = e.deptno
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
with searchable columns `name, job, department`, title `name`, subtitle `job`, badge `department`
|
|
1331
|
+
and link `{"page": 3, "items": {"P3_EMPNO": "#EMPNO#"}}`.
|
|
1332
|
+
|
|
1333
|
+
**The region**: attributes
|
|
1334
|
+
|
|
1335
|
+
| Key | Meaning |
|
|
1336
|
+
|---|---|
|
|
1337
|
+
| `configs` | The configurations it searches, e.g. `["CUSTOMERS", "ORDERS"]`; empty or left out: all of the application's |
|
|
1338
|
+
| `item` | Search with this page item's value (`"P1_SEARCH"`) instead of a field of the region's own; refresh the region from a dynamic action, or let the item submit the page |
|
|
1339
|
+
| `placeholder` | The field's placeholder |
|
|
1340
|
+
| `min_length` | Search only from this many characters (default 1) |
|
|
1341
|
+
| `empty` | The text when nothing is found (default "Nothing found.") |
|
|
1342
|
+
|
|
1343
|
+
The region's own field is a GET form (`?s<region id>=text`, so a search can be bookmarked) and works
|
|
1344
|
+
without JavaScript; with JavaScript it searches while the user types and replaces only the results.
|
|
1345
|
+
The Advisor plans every configuration with the columns it names and reports names a region uses
|
|
1346
|
+
that don't exist. Search configurations are exported with the application. The HR example's page 42
|
|
1347
|
+
searches employees, departments and the shortcuts list.
|
|
1348
|
+
|
|
1349
|
+
### `url` (URL region)
|
|
1350
|
+
|
|
1351
|
+
Shows another site's page in the region, in a sandboxed `<iframe>` (APEX: URL region).
|
|
1352
|
+
|
|
1353
|
+
**Source**: the URL. Its scheme and host are written out, so an item can never choose which site is
|
|
1354
|
+
shown; `&ITEM.` substitutions are allowed after the host and are URL-encoded:
|
|
1355
|
+
|
|
1356
|
+
```text
|
|
1357
|
+
https://reports.example.com/dashboard?customer=&P3_CUSTOMER_ID.&lang=&APP_LANGUAGE.
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
- `https://` only (`http://` for `localhost` during development), no user name or password in the
|
|
1361
|
+
URL. A path that starts with `/` shows a page of this server, such as another pgkiln application
|
|
1362
|
+
(`/a/portal/1`).
|
|
1363
|
+
- The page's Content-Security-Policy gets `frame-src` for exactly the origins of its visible URL
|
|
1364
|
+
regions; other pages frame nothing. The Advisor reports a URL that isn't allowed.
|
|
1365
|
+
- The other site decides whether it may be framed: many sites forbid it (`X-Frame-Options` or
|
|
1366
|
+
`frame-ancestors`), and the browser then shows an empty frame. Under the frame, *Open in a new tab*
|
|
1367
|
+
opens the page itself.
|
|
1368
|
+
- The region's title is the frame's accessible name.
|
|
1369
|
+
|
|
1370
|
+
Attributes:
|
|
1371
|
+
|
|
1372
|
+
| Key | Meaning |
|
|
1373
|
+
|---|---|
|
|
1374
|
+
| `height` | The frame's height in pixels, 100–4000 (default 480); the width is the region's |
|
|
1375
|
+
| `sandbox` | The frame's sandbox flags, from `allow-scripts`, `allow-forms`, `allow-popups`, `allow-same-origin`, `allow-downloads`, `allow-modals` and `allow-popups-to-escape-sandbox`. Default: the first four (most sites need them to work). `false`: no sandbox. Navigating the whole window (`allow-top-navigation`) is never allowed |
|
|
1376
|
+
| `fullscreen` | `true`: the framed page may go full screen (videos, maps) |
|
|
1377
|
+
|
|
1378
|
+
```json
|
|
1379
|
+
{"height": 640, "sandbox": ["allow-scripts", "allow-same-origin"], "fullscreen": true}
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1267
1382
|
### `static` and `dynamic` content
|
|
1268
1383
|
|
|
1269
1384
|
- **`static`**: `source` is HTML written by the developer, with `&ITEM.` substitutions
|
package/docs/guide/05-items.md
CHANGED
|
@@ -33,6 +33,8 @@ page, optionally inside a region. Name them `P<page>_<NAME>` (uppercase letters,
|
|
|
33
33
|
| `combobox` | free text with suggestions from the list of values; several values become tags | **colon-separated** values, e.g. `SQL:Sales` |
|
|
34
34
|
| `daterange` | two date pickers, *From* and *To* | `from:to`, e.g. `2026-01-01:2026-06-30` |
|
|
35
35
|
| `qrcode` | display only: the value as a QR code (SVG drawn on the server) | whatever was set |
|
|
36
|
+
| `image` | display only: a picture from a URL in the value, or a BLOB from a query ([Display image](#display-image-and-geocoded-address)) | a URL, or nothing |
|
|
37
|
+
| `address` | a text field with *Check address*: the installation's geocoder offers matching addresses ([Geocoded address](#display-image-and-geocoded-address)) | the address as text |
|
|
36
38
|
| `plugin` | an [item plug-in](04-pages-and-regions.md#plug-ins-with-their-own-code): a text field its JavaScript enhances; `config`: `{"plugin": "char_counter", "attributes": {"MAX": "140"}}` | text |
|
|
37
39
|
|
|
38
40
|
Use multi-value items in SQL with `string_to_array`:
|
|
@@ -70,6 +72,7 @@ Attributes (`config`):
|
|
|
70
72
|
| Key | Meaning |
|
|
71
73
|
|---|---|
|
|
72
74
|
| `submit_on_change` | `true`: submit the page when the value changes (typical for report filters) |
|
|
75
|
+
| `warn_unsaved` | `false`: a change of this item doesn't count for the page's [warn on unsaved changes](04-pages-and-regions.md#warn-on-unsaved-changes) |
|
|
73
76
|
| `null_label` | Text of the empty option in select lists (default `- Select -`); `false` removes the empty option |
|
|
74
77
|
| `cascade_parents` | Comma-separated items this list depends on, see [cascading lists](#cascading-lists-of-values) |
|
|
75
78
|
| `wide` | `true`: take the full width of the form |
|
|
@@ -129,6 +132,50 @@ the Markdown is shown as text, and the result passes through the same allow-list
|
|
|
129
132
|
|
|
130
133
|
The HR example's page 20 *Reviews* (`examples/hr/hr_20_items.sql`) uses all of them.
|
|
131
134
|
|
|
135
|
+
## Display image and geocoded address
|
|
136
|
+
|
|
137
|
+
**Display image** (`image`, APEX: *Display Image*) shows a picture and can't be submitted. Two
|
|
138
|
+
sources:
|
|
139
|
+
|
|
140
|
+
- **A URL in the item's value** (a computation, a process or a form fetch sets it): an `https://`
|
|
141
|
+
URL, a path on this server (`/a/hr/static/logo.png`) or a `data:image/png;base64,…` URL. Other
|
|
142
|
+
schemes (`http:`, `javascript:`, URLs with a user name) show nothing. The page's
|
|
143
|
+
Content-Security-Policy allows pictures from exactly the `https` origins its image items show
|
|
144
|
+
when it is rendered.
|
|
145
|
+
- **A BLOB from a query**: `{"sql": "select photo, photo_mime from hr.emp where empno = :P43_EMPNO::int"}`.
|
|
146
|
+
The first column of the first row is the picture (`bytea`), the second its MIME type (else it is
|
|
147
|
+
recognised from the bytes: PNG, JPEG, GIF, WebP). The browser loads it from
|
|
148
|
+
`/a/<alias>/<page>/image/<item>`, which runs the query as the application's role with the
|
|
149
|
+
session's item values, after the page's authorization and the item's own; it is never cached,
|
|
150
|
+
sandboxed and at most 20 MB.
|
|
151
|
+
|
|
152
|
+
`config`: `{"alt": "Photo of &P43_ENAME.", "width": 160, "height": 160}` (the alternative text,
|
|
153
|
+
`&ITEM.` substitutions allowed; it defaults to the label).
|
|
154
|
+
|
|
155
|
+
**Geocoded address** (`address`, APEX: *Geocoded Address*) is a text field with a *Check address*
|
|
156
|
+
button. The server asks the installation's geocoder for the typed text and shows up to five
|
|
157
|
+
matching addresses; the one chosen replaces the text, and with
|
|
158
|
+
`{"coordinates_item": "P43_LOCATION"}` its position goes into that item as `latitude,longitude`
|
|
159
|
+
(the format of a `location` item, so a map region can show it). `{"country": "nl"}` (or `"nl,be"`)
|
|
160
|
+
limits the search to those countries. The answers come in the user's language.
|
|
161
|
+
|
|
162
|
+
The geocoder is set per installation with **`GEOCODER_URL`**, any API that speaks Nominatim's
|
|
163
|
+
search (`?q=…&format=jsonv2`): your own [Nominatim](https://nominatim.org), LocationIQ
|
|
164
|
+
(`https://eu1.locationiq.com/v1/search` with `GEOCODER_KEY`) or, within its usage policy (an
|
|
165
|
+
absolute maximum of one request per second, no heavy use), `https://nominatim.openstreetmap.org/search`.
|
|
166
|
+
Each session may ask once a second. Without `GEOCODER_URL` an address item is a plain text item.
|
|
167
|
+
|
|
168
|
+
The HR example's page 43 *Data and settings* (`examples/hr/hr_53_data_settings.sql`) has both.
|
|
169
|
+
|
|
170
|
+
## Keeping values per user
|
|
171
|
+
|
|
172
|
+
`{"persist": "user"}` on any item (APEX: *Maintain session state: Per User*) keeps its value for
|
|
173
|
+
the signed-in user across sessions: when a page shows the item and the session has no value for it
|
|
174
|
+
yet, the user's last value comes back; a submit of the page (also a plain submit, e.g. *submit on
|
|
175
|
+
change*) saves the value the user could change, in the same transaction as the page's processes.
|
|
176
|
+
The value is a [user preference](06-processing.md#user-preferences) named like the item. Public
|
|
177
|
+
pages and applications without sign-in keep nothing.
|
|
178
|
+
|
|
132
179
|
## Lists of values
|
|
133
180
|
|
|
134
181
|
A list of values (LOV) provides the options of select lists, radio groups, checkbox groups,
|
|
@@ -167,6 +167,36 @@ end if;
|
|
|
167
167
|
All changes of the submit are rolled back, and the page is shown again with the entered
|
|
168
168
|
values so the user can correct them.
|
|
169
169
|
|
|
170
|
+
**Error handling function** (Settings → *Errors and sessions*; APEX: Error Handling Function). A
|
|
171
|
+
function of your own rewrites every error message users see: those raised in PL/pgSQL, the friendly
|
|
172
|
+
constraint messages and the "reference" messages of unexpected errors.
|
|
173
|
+
|
|
174
|
+
```sql
|
|
175
|
+
create function sales.error_message(e jsonb) returns jsonb language plpgsql as $$
|
|
176
|
+
begin
|
|
177
|
+
if e->>'constraint' = 'order_customer_fk' then
|
|
178
|
+
return jsonb_build_object('message', 'Choose an existing customer.');
|
|
179
|
+
end if;
|
|
180
|
+
if (e->>'internal')::boolean then
|
|
181
|
+
return jsonb_build_object('message', 'Something went wrong. Quote ' || (e->>'reference') || ' to the help desk.');
|
|
182
|
+
end if;
|
|
183
|
+
return null; -- keep the message
|
|
184
|
+
end $$;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It gets `message` (what pgkiln would show), `original_message`, `code` (SQLSTATE), `constraint`,
|
|
188
|
+
`table`, `column`, `detail`, `hint`, `component` (where it happened, e.g. `process "Save order"`),
|
|
189
|
+
`page`, `user`, `internal` (true for an unexpected error) and `reference` (its activity log id), and
|
|
190
|
+
returns `{"message": "…"}` or null. It runs as the application's role, in a transaction of its own,
|
|
191
|
+
with a 2-second limit; when it fails or returns null the message stays as it was. Debug mode shows
|
|
192
|
+
the real error and doesn't call it.
|
|
193
|
+
|
|
194
|
+
### Ajax callbacks
|
|
195
|
+
|
|
196
|
+
A process with point **ajax** runs when JavaScript calls it by name, `pgkiln.server.process('NAME')`
|
|
197
|
+
(APEX: On Demand processes, `apex.server.process`), and answers JSON; see
|
|
198
|
+
[chapter 7](07-dynamic-actions.md#ajax-callbacks).
|
|
199
|
+
|
|
170
200
|
### Debug messages
|
|
171
201
|
|
|
172
202
|
Like APEX's debug, pgkiln can record what each request of an application did and how long every
|
|
@@ -182,6 +212,10 @@ application's settings):
|
|
|
182
212
|
| 6 trace | Also each region, process, application process and computation with its duration, and the session (page, user, language, time zone) |
|
|
183
213
|
| 9 everything | Also the posted item values, computed values, skipped processes, hidden regions and other SQL notices |
|
|
184
214
|
|
|
215
|
+
A developer can also switch debug on for **their own session** only, from the
|
|
216
|
+
[developer toolbar](03-builder.md#developer-toolbar) on the running application: the application
|
|
217
|
+
stays out of debug for everyone else.
|
|
218
|
+
|
|
185
219
|
Each recorded request lists its entries with the time since the start of the request and, for
|
|
186
220
|
timed steps, their duration. Add your own from any application SQL (processes, region sources,
|
|
187
221
|
conditions, your PL/pgSQL functions), as with `apex_debug.message`:
|
|
@@ -240,6 +274,27 @@ a MIME type that doesn't look like one becomes `application/octet-stream`; the r
|
|
|
240
274
|
sniffed (`nosniff`), is sandboxed by its Content-Security-Policy and isn't cached. At most 1000
|
|
241
275
|
files and 100 MB per download.
|
|
242
276
|
|
|
277
|
+
#### Data export
|
|
278
|
+
|
|
279
|
+
With `{"export": "<format>"}` the query returns **rows**, and the download is those rows as one
|
|
280
|
+
file (APEX: `APEX_DATA_EXPORT`): `csv`, `json`, `xml`, `html`, `xlsx` (Excel) or `pdf`.
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{"export": "xlsx", "file_name": "employees-&P43_DEPTNO.", "title": "Employees"}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
| Key | Meaning |
|
|
287
|
+
|---|---|
|
|
288
|
+
| `export` | The format |
|
|
289
|
+
| `file_name` | The name without extension, `&ITEM.` substitutions allowed (default: the process name) |
|
|
290
|
+
| `title` | The title of an HTML or PDF file |
|
|
291
|
+
| `layout` | A [report layout](16-files.md#report-layouts) for the PDF (paper, margins, logo) |
|
|
292
|
+
| `separator` | CSV: one character instead of `,` |
|
|
293
|
+
|
|
294
|
+
Column names are the headings (`hire_date` → *Hire Date*, translated like report headings in Excel
|
|
295
|
+
and PDF). Excel keeps numbers and dates as numbers and dates; Excel and PDF hold at most 100,000
|
|
296
|
+
rows. From SQL, `meta.data_export` makes the same CSV, JSON, XML and HTML ([below](#data-export-from-sql)).
|
|
297
|
+
|
|
243
298
|
### Execution chains
|
|
244
299
|
|
|
245
300
|
A `chain` process runs its **children**: the processes of the page whose `parent_process` is the
|
|
@@ -295,6 +350,30 @@ looks up the employee (a `sql` child) and then starts the ONBOARDING workflow (a
|
|
|
295
350
|
*Stop onboarding* terminates it; *Year-end check* is a background chain whose jobs the region
|
|
296
351
|
"My background jobs" lists from `meta.process_jobs`.
|
|
297
352
|
|
|
353
|
+
### Send e-mail
|
|
354
|
+
|
|
355
|
+
A `send_email` process (APEX: *Send E-Mail*) queues a mail through the application's [mail
|
|
356
|
+
service](#sending-e-mail), without SQL:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{"to": "&P5_EMAIL.", "cc": "&P5_MANAGER_EMAIL.", "subject": "Order &P5_ID. shipped",
|
|
360
|
+
"body": "Dear &P5_NAME., your order is on its way.",
|
|
361
|
+
"body_html": "<p>Dear &P5_NAME.,</p><p>your order is on its way.</p>",
|
|
362
|
+
"attachments": "select content, filename, mime from sales.invoice where order_id = :P5_ID"}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
or with an [e-mail template](#e-mail-templates):
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{"to": "&P5_EMAIL.", "template": "ORDER_SHIPPED", "placeholders": {"NAME": "&P5_NAME.", "ORDER_ID": "&P5_ID."}}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`to`, `cc`, `bcc` (several addresses separated by commas), `from`, `reply_to`, `subject` and `body`
|
|
372
|
+
take `&ITEM.` substitutions as they are; in `body_html` the item values are HTML-escaped.
|
|
373
|
+
`attachments` is a SELECT (binds allowed) with the columns `content` (bytea), `filename` and `mime`,
|
|
374
|
+
one attachment per row. The mail is queued in the page's transaction: when a later process fails,
|
|
375
|
+
nothing is sent. It can run in a background chain too.
|
|
376
|
+
|
|
298
377
|
### Send push notification
|
|
299
378
|
|
|
300
379
|
A `send_push` process (APEX: *Send Push Notification*) sends a [push
|
|
@@ -514,6 +593,61 @@ select (select empno from hr.emp where lower(username) = lower(:APP_USER)) as ai
|
|
|
514
593
|
|
|
515
594
|
`&AI_ENAME.` then greets the user on the dashboard.
|
|
516
595
|
|
|
596
|
+
## Application settings
|
|
597
|
+
|
|
598
|
+
**Shared Components → Application settings** (APEX: *Application Settings*, `APEX_APP_SETTING`)
|
|
599
|
+
are named values an administrator changes after deployment: a support address, a limit, a switch.
|
|
600
|
+
Each has a name (`SUPPORT_EMAIL`), a value, a description, optionally a list of **valid values**,
|
|
601
|
+
and *Value required*. Application code reads and changes them:
|
|
602
|
+
|
|
603
|
+
```sql
|
|
604
|
+
select meta.get_app_setting('SUPPORT_EMAIL');
|
|
605
|
+
select meta.set_app_setting('LEAVE_APPROVAL', 'hr'); -- one of its valid values
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Both work for the current application only, called as its own database role (application code),
|
|
609
|
+
and raise an error for an unknown name, a value outside the valid values or an empty required one.
|
|
610
|
+
Settings are exported with the application, values included. An import over an installed
|
|
611
|
+
application (CLI, MCP, a working copy merge) keeps the **installed** value of every setting marked
|
|
612
|
+
*Keep the installed value on upgrade* (the default); with the mark off, the file's value wins. When
|
|
613
|
+
the application's build status is *Run only*, its existing settings can still be edited.
|
|
614
|
+
|
|
615
|
+
## User preferences
|
|
616
|
+
|
|
617
|
+
Values kept per user and application across sessions (APEX: `APEX_UTIL.GET_PREFERENCE`,
|
|
618
|
+
`SET_PREFERENCE`, `REMOVE_PREFERENCE`):
|
|
619
|
+
|
|
620
|
+
```sql
|
|
621
|
+
select meta.set_preference('report.rows', '50'); -- the signed-in user
|
|
622
|
+
select meta.get_preference('report.rows'); -- null when there is none
|
|
623
|
+
select meta.remove_preference('report.rows');
|
|
624
|
+
select meta.get_preference('report.rows', 'blake'); -- another user of the application
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Names are letters, digits, `_`, `.` and `-` (at most 100); values at most 32,000 bytes; at most 500
|
|
628
|
+
preferences per user. Setting one needs a signed-in user. Items with `{"persist": "user"}` keep their
|
|
629
|
+
values this way ([Keeping values per user](05-items.md#keeping-values-per-user)). Preferences are data of
|
|
630
|
+
the installation: not exported, kept by an import over the application, deleted with it.
|
|
631
|
+
|
|
632
|
+
## Data export from SQL
|
|
633
|
+
|
|
634
|
+
`meta.data_export(query, format, options)` (APEX: `APEX_DATA_EXPORT.EXPORT`) returns a query's
|
|
635
|
+
rows as a file (`bytea`): `csv`, `json` (an array of objects), `xml` (`query_to_xml`) or `html` (a
|
|
636
|
+
table). It runs with the **caller's** privileges, so grants and row level security apply as to the
|
|
637
|
+
query itself. Attach the file to a mail, store it, or send it with a download process:
|
|
638
|
+
|
|
639
|
+
```sql
|
|
640
|
+
select meta.add_mail_attachment(
|
|
641
|
+
meta.send_mail('boss@example.com', 'Staff', 'Attached.'),
|
|
642
|
+
meta.data_export('select empno, ename, sal from hr.emp order by ename', 'csv', '{"separator": ";"}'),
|
|
643
|
+
'staff.csv', 'text/csv');
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Options: `separator` (CSV, one character), `header` (`false`: no heading line), `bom` (`false`: no
|
|
647
|
+
UTF-8 byte order mark, which Excel needs to read UTF-8), `title` (HTML). CSV fields are quoted when
|
|
648
|
+
needed, and text that a spreadsheet would run as a formula (`=`, `+`, `-`, `@`) gets a leading
|
|
649
|
+
apostrophe, as in report downloads. Excel and PDF come from the [download process](#data-export).
|
|
650
|
+
|
|
517
651
|
## Automations
|
|
518
652
|
|
|
519
653
|
**Shared Components → Automations** run SQL or PL/pgSQL on a schedule, like APEX automations:
|
|
@@ -604,6 +738,79 @@ creates or replaces the single action (the column itself stays empty).
|
|
|
604
738
|
|
|
605
739
|
If you prefer the database to schedule work, [pg_cron](15-extensions.md) still works next to this.
|
|
606
740
|
|
|
741
|
+
## Sending e-mail
|
|
742
|
+
|
|
743
|
+
Like APEX's `APEX_MAIL`, applications send e-mail from SQL, from a *Send e-mail* process and from
|
|
744
|
+
e-mail templates. Where APEX sends only through an SMTP server with a user name and password,
|
|
745
|
+
pgkiln sends through any of these **mail services** (Workspace utilities → [Mail
|
|
746
|
+
services](03-builder.md#mail-services), administrators):
|
|
747
|
+
|
|
748
|
+
| Provider | How pgkiln hands mail over | Credentials |
|
|
749
|
+
|---|---|---|
|
|
750
|
+
| SMTP | Any SMTP server or relay, STARTTLS or TLS | User name and password, or **OAuth2** (SASL XOAUTH2 with a token from the client credentials grant: Microsoft Entra for Exchange Online, or another identity provider's token URL) |
|
|
751
|
+
| Microsoft 365 (Graph) | Microsoft Graph `sendMail` as the From mailbox | An Entra application (tenant, client ID, client secret) with the application permission **Mail.Send**; restrict it to the sending mailboxes with an application access policy |
|
|
752
|
+
| Amazon SES | SES API v2, the raw MIME message, Signature Version 4 | Region, access key ID and secret access key (an IAM user allowed `ses:SendEmail`) |
|
|
753
|
+
| Resend, Postmark, SendGrid, Mailgun | Their HTTP APIs | An API key (Postmark: a server token; Mailgun: also the sending domain and region) |
|
|
754
|
+
| Log only | Nothing is sent: the server's log shows each mail | – (development and demos) |
|
|
755
|
+
|
|
756
|
+
Microsoft retires basic authentication (a password) for SMTP in Exchange Online: use SMTP with OAuth2
|
|
757
|
+
or the Graph provider. Secrets are stored encrypted with `PGKILN_SECRET_KEY`, are write-only in the
|
|
758
|
+
builder, never exported and never readable by applications. Each service lists the **From
|
|
759
|
+
addresses** it may send as (an address, or `@example.com` for a whole domain); an administrator
|
|
760
|
+
chooses each application's service, its From address and an optional **limit per day**.
|
|
761
|
+
|
|
762
|
+
**How mail is sent.** `meta.send_mail` and the others only *queue* the mail, in the caller's
|
|
763
|
+
transaction: a transaction that rolls back sends nothing. After the commit, the server sends it at
|
|
764
|
+
once (`NOTIFY`), or on the scheduler's next pass. A failure that may pass (a network problem, HTTP
|
|
765
|
+
429 or 5xx, an SMTP 4xx answer) is tried again after 1, 4, 16 and 64 minutes; the fifth failure, or a
|
|
766
|
+
refusal (an unknown recipient, a sender the provider doesn't accept), marks the mail *error*.
|
|
767
|
+
Applications → Activity → **Mail** shows each mail with its outcome, and sends a failed one again or
|
|
768
|
+
cancels a queued one. Mail is kept 30 days.
|
|
769
|
+
|
|
770
|
+
### From SQL
|
|
771
|
+
|
|
772
|
+
```sql
|
|
773
|
+
-- APEX_MAIL.SEND
|
|
774
|
+
select meta.send_mail(
|
|
775
|
+
p_to => 'jane@example.com, john@example.com',
|
|
776
|
+
p_subject => 'Your leave was approved',
|
|
777
|
+
p_body => 'Enjoy your holiday!', -- plain text
|
|
778
|
+
p_body_html => '<p>Enjoy your <b>holiday</b>!</p>', -- HTML (optional)
|
|
779
|
+
p_cc => null, p_bcc => null, p_reply_to => 'hr@example.com',
|
|
780
|
+
p_from => null); -- null: the application's From address
|
|
781
|
+
|
|
782
|
+
-- APEX_MAIL.ADD_ATTACHMENT, in the same transaction
|
|
783
|
+
with m as (select meta.send_mail('jane@example.com', 'Invoice 42', 'Attached.') as id)
|
|
784
|
+
select meta.add_mail_attachment(m.id, i.pdf, 'invoice-42.pdf', 'application/pdf')
|
|
785
|
+
from m, sales.invoice i where i.id = 42;
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
See [chapter 9](09-reference.md#e-mail-from-sql) for every function and its limits. The functions
|
|
789
|
+
work for the **current application** only when the caller runs as that application's database role
|
|
790
|
+
(pages, processes, automations, workflows and background chains do); the SQL Workshop is not an
|
|
791
|
+
application.
|
|
792
|
+
|
|
793
|
+
### E-mail templates
|
|
794
|
+
|
|
795
|
+
Shared Components → **E-mail templates** (APEX: Email Templates): a name, a subject, an HTML body
|
|
796
|
+
and a plain text body with `#PLACEHOLDERS#`. In the HTML body `#NAME#` is the value HTML-escaped and
|
|
797
|
+
`#NAME!RAW#` the value as it is (trusted HTML only); in the subject and the plain text the value is
|
|
798
|
+
as it is. Names are case-insensitive; a placeholder without a value stays as written.
|
|
799
|
+
|
|
800
|
+
```sql
|
|
801
|
+
select meta.send_mail_template('LEAVE_DECIDED',
|
|
802
|
+
jsonb_build_object('NAME', 'Jane', 'STATUS', 'approved', 'START', '1 Aug 2026'),
|
|
803
|
+
'jane@example.com');
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
Templates are exported with the application; mail services, the application's service and the
|
|
807
|
+
mail log belong to the installation and are not.
|
|
808
|
+
|
|
809
|
+
**HR example.** The template `LEAVE_DECIDED` tells an employee that a leave request was approved or
|
|
810
|
+
rejected: a trigger on `hr.leave_request` queues it when the status changes. The example's mail
|
|
811
|
+
service `DEVELOPMENT_LOG` is of provider *log only*, so nothing leaves the server until an
|
|
812
|
+
administrator gives HR a real service.
|
|
813
|
+
|
|
607
814
|
## Approvals and the task list
|
|
608
815
|
|
|
609
816
|
Approvals (APEX: Task Definitions, the Approvals component and the Unified Task List) let an
|
|
@@ -676,8 +883,9 @@ open tasks so they don't linger: `select meta.close_tasks('EXPENSE_APPROVAL', :P
|
|
|
676
883
|
| `meta.task_events` | View: the history and comments of those tasks |
|
|
677
884
|
|
|
678
885
|
The task tables themselves are closed to the application's role: rights are checked by these
|
|
679
|
-
functions and views, for every call.
|
|
680
|
-
|
|
886
|
+
functions and views, for every call. To tell owners by e-mail, call
|
|
887
|
+
[`meta.send_mail`](#sending-e-mail) from the task definition's SQL or from an automation that
|
|
888
|
+
reminds owners of overdue tasks; or put a task list on the home page.
|
|
681
889
|
|
|
682
890
|
**Example:** in the HR sample application, a leave request creates a `LEAVE_APPROVAL` task for the
|
|
683
891
|
employee's manager; *My tasks* (page 14) approves or rejects it through `hr.decide_leave`, the same
|
|
@@ -10,8 +10,8 @@ your own from a static application file.
|
|
|
10
10
|
|
|
11
11
|
| Property | Meaning |
|
|
12
12
|
|---|---|
|
|
13
|
-
| `event` |
|
|
14
|
-
| `trigger_element` |
|
|
13
|
+
| `event` | When it runs, see [Events](#events) |
|
|
14
|
+
| `trigger_element` | What it listens to: item names (`change`, `focus`, `blur`, `key_release`), a button name (`click`), region ids or static ids (`after_refresh`, `selection_change`), dialog page numbers (`dialog_closed`; empty: any dialog) or event names (`custom`), comma separated. Empty for `load` and `before_submit` |
|
|
15
15
|
| `condition_type`, `condition_value` | Optional client-side condition on the (first) trigger item's value (for `dialog_closed`: on the dialog's page number): `equals`, `not_equals`, `in_list` (comma-separated values), `is_null`, `is_not_null` |
|
|
16
16
|
| `action` | What to do, see below |
|
|
17
17
|
| `affected_items`, `affected_region_id` | What it acts on |
|
|
@@ -23,6 +23,31 @@ your own from a static application file.
|
|
|
23
23
|
| `authz` | Only for authorized users |
|
|
24
24
|
| `build_option` | Only while the [build option](06-processing.md#build-options) is included |
|
|
25
25
|
|
|
26
|
+
## Events
|
|
27
|
+
|
|
28
|
+
| Event | Runs when | Trigger |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `change` | An item's value changed | Item names |
|
|
31
|
+
| `click` | A button was clicked | A button name |
|
|
32
|
+
| `load` | The page has loaded | – |
|
|
33
|
+
| `dialog_closed` | A modal dialog opened from the page was submitted and closed ([below](#dialog-closed)) | Dialog page numbers (empty: any) |
|
|
34
|
+
| `focus` / `blur` | An item gets / loses the focus (APEX: Get Focus, Lose Focus) | Item names |
|
|
35
|
+
| `key_release` | A key is released in an item | Item names |
|
|
36
|
+
| `after_refresh` | A region was refreshed (`refresh_region`), loaded lazily or shown for another master row | Region ids or static ids |
|
|
37
|
+
| `selection_change` | Rows are selected in a report with [row selection](04-pages-and-regions.md#row-selection), or another master row in a grid; the condition sees the selected values, colon separated | Region ids or static ids |
|
|
38
|
+
| `before_submit` | The page is about to be submitted; a declined `confirm` keeps the page | – |
|
|
39
|
+
| `custom` | JavaScript raised the event: `pgkiln.trigger('ORDER_SAVED', value)` (APEX: custom events, `apex.event.trigger`) | Event names (case-insensitive) |
|
|
40
|
+
|
|
41
|
+
**The actions of one event and trigger run one after the other, in sequence order** (APEX: the true
|
|
42
|
+
actions of a dynamic action), each waiting for the one before (a server action for its answer). A
|
|
43
|
+
`confirm` that is declined stops the actions after it, and for `before_submit` the submit too:
|
|
44
|
+
|
|
45
|
+
| Sequence | Event | Action | Message / items |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| 10 | `click` on `DELETE_ALL` | `confirm` | *Delete every selected row?* |
|
|
48
|
+
| 20 | `click` on `DELETE_ALL` | `execute_sql` | `delete from sales.order_line where id = any(string_to_array(:P5_SELECTED, ':')::int[])` |
|
|
49
|
+
| 30 | `click` on `DELETE_ALL` | `refresh_region` | the order lines |
|
|
50
|
+
|
|
26
51
|
## Actions
|
|
27
52
|
|
|
28
53
|
| Action | Runs in | Effect |
|
|
@@ -45,11 +70,44 @@ your own from a static application file.
|
|
|
45
70
|
| `plugin` | browser | Runs the [dynamic action plug-in](04-pages-and-regions.md#plug-ins-with-their-own-code) named in `code`, with `config` `{"attributes": {…}}` (`&ITEM.` filled in); the function gets the same context as *Execute JavaScript* plus `attributes` |
|
|
46
71
|
| `execute_javascript` | browser | Calls the function named in `code`, which a [static application file](03-builder.md#static-application-files) registered; see [below](#execute-javascript) |
|
|
47
72
|
| `push_subscribe` | browser | Turns on [push notifications](17-mobile.md#push-notifications) on the user's device: the browser asks for permission, then the device is registered. Use it with `click` (browsers ask only after one). `message` is shown when it worked, the browser's reason when not. The app needs push notifications on |
|
|
73
|
+
| `confirm` | browser | Asks `message`; **No stops the actions after it** (and the submit, for `before_submit`) |
|
|
74
|
+
| `close_dialog` | browser | In a modal page: closes it; the page below refreshes, or runs its `dialog_closed` actions (APEX: Close Dialog) |
|
|
75
|
+
| `cancel_dialog` | browser | In a modal page: closes it without refreshing the page below |
|
|
76
|
+
| `clear` | browser | Empties the affected items |
|
|
77
|
+
| `open_region` / `close_region` | browser | Opens or closes a *collapsible* region; another region is shown or hidden |
|
|
78
|
+
| `download` | server | Downloads the file of the [download process](06-processing.md#download) named in `code` (a process of the page, with its authorization and condition), after sending `items_to_submit`; the page stays as it is |
|
|
79
|
+
| `print` | browser | The browser's print dialog |
|
|
80
|
+
| `current_position` | browser | The device's position as `latitude,longitude` (5 decimals) into the first affected item |
|
|
81
|
+
| `share` | browser | The device's share sheet with the page's title, `message` and address; where there is none, the address is copied |
|
|
48
82
|
|
|
49
83
|
Server-side actions first store `items_to_submit` in session state, run as the application's
|
|
50
84
|
database role like everything else, and check the page's and the dynamic action's authorization.
|
|
51
85
|
The SQL itself never reaches the browser.
|
|
52
86
|
|
|
87
|
+
## Ajax callbacks
|
|
88
|
+
|
|
89
|
+
A page process with point **ajax** (APEX: an On Demand process) runs only when JavaScript calls it by
|
|
90
|
+
name, and answers JSON; an application process with point *ajax* can be called from every page (a
|
|
91
|
+
page's own process of that name wins). Only processes of type `sql` answer.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
// in a static application file
|
|
95
|
+
const r = await pgkiln.server.process('ORDER_TOTAL', { items: ['P5_ORDER_ID'], x01: 'EUR' });
|
|
96
|
+
r.data.total; // the first row of the process's last statement
|
|
97
|
+
r.rows; // its rows (at most 1000)
|
|
98
|
+
r.items; // the page and application items it set (also set on the page)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
-- the process ORDER_TOTAL (point ajax): :X01 … :X10 are the x01 … x10 sent
|
|
103
|
+
select sum(qty * price) as total, :X01 as currency from sales.order_line where order_id = :P5_ORDER_ID::int
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`items` is a list of item names (their current values are sent) or `{"P5_ID": "7"}`; only items the
|
|
107
|
+
user may change are set, as on a submit. The process runs as the application's role with its
|
|
108
|
+
authorization, condition and build option; errors come back as `{ "error": "…" }` (the promise
|
|
109
|
+
rejects). The request carries the page's CSRF token and needs page access.
|
|
110
|
+
|
|
53
111
|
## Execute JavaScript
|
|
54
112
|
|
|
55
113
|
APEX's *Execute JavaScript Code* runs code typed into the action. pgkiln's Content-Security-Policy
|
|
@@ -47,8 +47,9 @@ The builder and the runtime cover what APEX offers for its accounts:
|
|
|
47
47
|
The `meta.set_password`/`expire`/`unexpire` functions are owner-only. Grant them to an app role if
|
|
48
48
|
you want to build user administration pages inside an application.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
There is no self-service "forgot password" link (APEX apps don't have one out of the box either;
|
|
51
|
+
an application can build one with [e-mail](06-processing.md#sending-e-mail) and its own token
|
|
52
|
+
table). A user who forgot their password asks an administrator, who
|
|
52
53
|
sets a temporary one under **Users → account → Set password**, with *Require change of password
|
|
53
54
|
on first use* ticked (the default).
|
|
54
55
|
|
|
@@ -327,6 +328,16 @@ like for other users (an account of the same name, if there is one), or from the
|
|
|
327
328
|
in authorization schemes. The login page has the user name and password form only: *Keep me signed
|
|
328
329
|
in*, LDAP, single sign-on and the password-change form are not used by these apps.
|
|
329
330
|
|
|
331
|
+
### Session timeout warning
|
|
332
|
+
|
|
333
|
+
Sessions end after the idle time (`SESSION_IDLE_MINUTES`, or Instance settings) and at their maximum
|
|
334
|
+
length (`SESSION_MAX_HOURS`). Signed-in users are warned before (Settings → *Errors and sessions* →
|
|
335
|
+
*Session timeout warning*, default 120 seconds; 0: no warning; APEX: Session Timeout Warning): a
|
|
336
|
+
dialog says when the session ends and offers **Stay signed in**, which marks the session active
|
|
337
|
+
(POST `/a/<alias>/session/extend`, with the session's CSRF token). At the maximum length the user can
|
|
338
|
+
only save their work; when the session has ended the dialog offers to sign in again and comes back
|
|
339
|
+
to the page.
|
|
340
|
+
|
|
330
341
|
### Keep me signed in
|
|
331
342
|
|
|
332
343
|
Under **Settings → Sign-in methods**, *"Keep me signed in" for (days)* (1–365) adds a checkbox to the
|
|
@@ -13,6 +13,11 @@ triggers, your own functions).
|
|
|
13
13
|
| `meta.jwt_claims()` | jsonb | The verified JWT claims of a PostgREST request, or NULL |
|
|
14
14
|
| `meta.app_language()` | text | The language of the request (`nl`, `en-GB`, …) |
|
|
15
15
|
| `meta.message(name, variadic params)` | text | A text message in the current language, `%0`…`%9` replaced; falls back to the base and primary language |
|
|
16
|
+
| `meta.lang(text, language default null)` | text | A [dynamic translation](14-globalization.md#dynamic-translations) of a value from a table in the session's (or the given) language, else the text (APEX: `APEX_LANG.LANG`) |
|
|
17
|
+
| `meta.get_app_setting(name)`, `meta.set_app_setting(name, value)` | text / void | An [application setting](06-processing.md#application-settings) of the current application (APEX: `APEX_APP_SETTING`); application code only |
|
|
18
|
+
| `meta.get_preference(name, user default null)`, `meta.set_preference(name, value, user default null)`, `meta.remove_preference(name, user default null)` | text / void | A [user preference](06-processing.md#user-preferences) of the signed-in (or the given) user in the current application (APEX: `APEX_UTIL.*_PREFERENCE`) |
|
|
19
|
+
| `meta.data_export(query, format default 'csv', options jsonb default '{}')` | bytea | A query's rows as a CSV, JSON, XML or HTML file, run with the caller's privileges ([data export](06-processing.md#data-export-from-sql); APEX: `APEX_DATA_EXPORT`) |
|
|
20
|
+
| `meta.csv_field(value, separator default ',', is_text default true)` | text | One CSV field: quoted when needed, a formula neutralised |
|
|
16
21
|
| `meta.password_days_left(username)` | int | Days until the password expires (0: must change now, NULL: never) |
|
|
17
22
|
| `meta.v(name)` | text | The session-state value of an item (use it inside functions and `DO` blocks) |
|
|
18
23
|
| `meta.set_tenant(tenant)` | void | Sets (or with NULL or `''` clears) the current session's tenant (APEX: `apex_session.set_tenant_id`): at once and for the session's next requests. Workflows and tasks then carry it and show only to that tenant ([chapter 6](06-processing.md#tenants)) |
|
|
@@ -93,6 +98,22 @@ select meta.web_request_source('EXCHANGE', '{"currency": "EUR"}');
|
|
|
93
98
|
- Application roles can't read `meta.web_request_log`; the owner can, e.g. in the SQL Workshop:
|
|
94
99
|
`select id, status, status_code, url, message from meta.web_request_log order by id desc`.
|
|
95
100
|
|
|
101
|
+
### E-mail from SQL
|
|
102
|
+
|
|
103
|
+
[Sending e-mail](06-processing.md#sending-e-mail) (APEX: `APEX_MAIL`). The application needs a mail
|
|
104
|
+
service (an administrator chooses it under Workspace utilities → Mail services).
|
|
105
|
+
|
|
106
|
+
| Function | Returns |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `meta.send_mail(p_to, p_subject, p_body, p_body_html, p_from, p_cc, p_bcc, p_reply_to)` | Queues a mail and returns its id (APEX_MAIL.SEND). `p_to`, `p_cc`, `p_bcc`: addresses separated by commas or semicolons, at most 50 together (plain addresses, no display names); `p_subject`: one line, at most 998 characters; `p_body` (plain text) and/or `p_body_html`, each at most 1 MB; `p_from`: an address the service may send as (default: the application's); `p_reply_to`: one address |
|
|
109
|
+
| `meta.send_mail_template(p_template, p_placeholders jsonb, p_to, p_cc, p_bcc, p_from, p_reply_to)` | The same with an [e-mail template](06-processing.md#e-mail-templates) and its placeholder values (APEX_MAIL.SEND with `p_template_static_id`) |
|
|
110
|
+
| `meta.add_mail_attachment(p_mail_id, p_content bytea, p_filename, p_mime)` | Adds a file to a mail queued **in the same transaction** (APEX_MAIL.ADD_ATTACHMENT); at most 20 files and 20 MB per mail, 10 MB per file |
|
|
111
|
+
| `meta.mail_status(p_mail_id)` | `queued`, `sending`, `sent`, `error` or `cancelled`; NULL for another application's mail |
|
|
112
|
+
|
|
113
|
+
The mail is sent by the server **after the transaction commits**. Over the application's limit per
|
|
114
|
+
day `meta.send_mail` raises an error. Application roles can't read the queue; the owner can:
|
|
115
|
+
`select id, app_id, to_addresses, subject, status, attempts, message from meta.mail_queue order by id desc`.
|
|
116
|
+
|
|
96
117
|
### Push notifications from SQL
|
|
97
118
|
|
|
98
119
|
APEX_PWA push notifications ([chapter 17](17-mobile.md#push-notifications)); the application needs
|
|
@@ -52,7 +52,11 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
|
|
|
52
52
|
| Pagination *Row Ranges X to Y*, *Maximum Row Count*, region *Lazy Loading*, *Server Cache* | `"pagination": "range"`, `max_rows`, `"lazy": true`, `"cache": {"scope", "seconds"}` ([large tables](04-pages-and-regions.md#large-tables)) |
|
|
53
53
|
| `APEX_UTIL.CHANGE_CURRENT_USER_PW`, `RESET_PASSWORD`, `EXPIRE_END_USER_ACCOUNT` | My account page; `meta.set_password()`, `meta.expire_password()` ([chapter 8](08-security.md#passwords-and-my-account)) |
|
|
54
54
|
| Progressive Web App push notifications: `APEX_PWA.SEND_PUSH_NOTIFICATION`, `HAS_PUSH_SUBSCRIPTION`, *Send Push Notification* process, the subscription settings page | `meta.send_push(user, title, body, page, items)`, `meta.has_push_subscription(user)`, process type `send_push`, *My account → Notifications* and the dynamic action `push_subscribe` ([chapter 17](17-mobile.md#push-notifications)) |
|
|
55
|
-
| `APEX_MAIL`, Send E-Mail process, e-mail templates |
|
|
55
|
+
| `APEX_MAIL`, Send E-Mail process, e-mail templates | `meta.send_mail`, `meta.send_mail_template`, `meta.add_mail_attachment`, the `send_email` process and Shared Components → E-mail templates (`#NAME#` placeholders) ([chapter 6](06-processing.md#sending-e-mail)). Instead of the instance's SMTP settings: mail services with SMTP (password or OAuth2), Microsoft Graph, Amazon SES, Resend, Postmark, SendGrid or Mailgun. `APEX_MAIL.PUSH_QUEUE` is not needed: mail goes out right after the commit |
|
|
56
|
+
| `APEX_APP_SETTING`, `APEX_UTIL.GET/SET/REMOVE_PREFERENCE`, *Maintain session state: Per User* | Shared Components → Application settings and `meta.get_app_setting`/`meta.set_app_setting`; `meta.get_preference`/`set_preference`/`remove_preference`; `{"persist": "user"}` on an item ([chapter 6](06-processing.md#application-settings)) |
|
|
57
|
+
| `APEX_DATA_EXPORT.EXPORT` | `meta.data_export(query, format, options)` (CSV, JSON, XML, HTML) and the download process's `{"export": "xlsx"}` (also PDF) ([chapter 6](06-processing.md#data-export-from-sql)) |
|
|
58
|
+
| `APEX_LANG.LANG`, dynamic translations | Shared Components → Dynamic translations and `meta.lang(text)` ([chapter 14](14-globalization.md#dynamic-translations)) |
|
|
59
|
+
| Display Image, Geocoded Address items | Item types `image` and `address` (geocoder: `GEOCODER_URL`) ([chapter 5](05-items.md#display-image-and-geocoded-address)) |
|
|
56
60
|
| Translated applications (XLIFF), `APEX_LANG.MESSAGE`, `&APP_TEXT$NAME.` | Translations in the app (XLIFF/CSV import and export), `meta.message()`, `&APP_TEXT$NAME.` ([chapter 14](14-globalization.md)) |
|
|
57
61
|
| Application date format mask | Settings → Globalization → Date format (Oracle-style masks) |
|
|
58
62
|
| Number format masks (`FML999G999G990D00`) on columns and items | `{"formats": {...}}` on report, grid and cards columns, `{"format_mask": "..."}` on charts and number/display items ([chapter 14](14-globalization.md#number-formats)) |
|