pgkiln 0.51.0 → 0.53.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 +1 -1
- package/SECURITY.md +10 -1
- package/db/migrations/121_ai_agents.sql +94 -0
- package/db/migrations/122_team_development.sql +186 -0
- package/db/migrations/123_workflow_outcomes.sql +407 -0
- package/db/migrations/124_component_details.sql +51 -0
- package/db/migrations/125_app_change.sql +23 -0
- package/db/migrations/126_mail_queue_network_access.sql +119 -0
- package/docs/apex-feature-parity.md +50 -19
- package/docs/guide/01-installation.md +8 -4
- package/docs/guide/03-builder.md +197 -10
- package/docs/guide/04-pages-and-regions.md +71 -21
- package/docs/guide/05-items.md +7 -1
- package/docs/guide/06-processing.md +129 -9
- package/docs/guide/07-dynamic-actions.md +34 -0
- package/docs/guide/08-security.md +55 -0
- package/docs/guide/09-reference.md +5 -2
- package/docs/guide/11-from-apex.md +3 -1
- package/docs/guide/12-development.md +87 -16
- package/docs/guide/14-globalization.md +49 -1
- package/docs/guide/18-cli.md +21 -1
- package/docs/guide/19-rest-data-sources.md +12 -0
- package/docs/guide/20-ai-agents.md +12 -4
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_64_ai_agent.sql +97 -0
- package/examples/hr/hr_65_ask_reports.sql +21 -0
- package/examples/hr/hr_66_team_development.sql +58 -0
- package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
- package/examples/hr/hr_68_component_details.sql +96 -0
- package/examples/hr/hr_70_data_security.sql +79 -0
- package/package.json +1 -1
- package/public/app.css +135 -21
- package/public/app.js +134 -20
- package/public/builder.css +133 -1
- package/public/builder.js +51 -0
- package/src/ai/chat.ts +18 -18
- package/src/ai/service.ts +35 -11
- package/src/ai/types.ts +7 -2
- package/src/app.ts +15 -0
- package/src/builder/advisor.ts +6 -3
- package/src/builder/ai-agents.ts +372 -0
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/ai.ts +13 -9
- package/src/builder/app-updates.ts +55 -0
- package/src/builder/assistant.ts +22 -7
- package/src/builder/components.ts +38 -9
- package/src/builder/data-security.ts +182 -0
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +6 -1
- package/src/builder/home.ts +16 -5
- package/src/builder/locks.ts +7 -1
- package/src/builder/mail-queue.ts +212 -0
- package/src/builder/mail.ts +72 -45
- package/src/builder/network-access.ts +140 -0
- package/src/builder/object-actions.ts +1 -0
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/region-help.ts +2 -2
- package/src/builder/rest-catalogs.ts +19 -4
- package/src/builder/routes.ts +4 -1
- package/src/builder/shared.ts +1 -1
- package/src/builder/sql.ts +2 -0
- package/src/builder/team.ts +991 -0
- package/src/builder/themeroller.ts +47 -6
- package/src/builder/ui.ts +7 -1
- package/src/builder/websources.ts +1 -1
- package/src/builder/workflow-migrate.ts +210 -0
- package/src/builder/workflows.ts +29 -6
- package/src/builder/workspace-home.ts +2 -0
- package/src/builder/workspace-mail.ts +121 -0
- package/src/builder/workspace-transfer.ts +1 -1
- package/src/builder/workspaces.ts +1 -0
- package/src/cli/main.ts +25 -3
- package/src/cli/mcp.ts +40 -4
- package/src/cli/only.ts +52 -0
- package/src/cli/replace.ts +11 -1
- package/src/datasec.ts +343 -0
- package/src/i18n/ar.ts +17 -0
- package/src/i18n/cs.ts +17 -0
- package/src/i18n/da.ts +17 -0
- package/src/i18n/de.ts +17 -0
- package/src/i18n/el.ts +17 -0
- package/src/i18n/es.ts +17 -0
- package/src/i18n/fi.ts +17 -0
- package/src/i18n/fr.ts +17 -0
- package/src/i18n/he.ts +17 -0
- package/src/i18n/it.ts +17 -0
- package/src/i18n/ja.ts +17 -0
- package/src/i18n/ko.ts +17 -0
- package/src/i18n/nb.ts +17 -0
- package/src/i18n/pl.ts +17 -0
- package/src/i18n/pt.ts +17 -0
- package/src/i18n/ru.ts +17 -0
- package/src/i18n/sv.ts +17 -0
- package/src/i18n/tr.ts +17 -0
- package/src/i18n/uk.ts +17 -0
- package/src/i18n/zh.ts +17 -0
- package/src/i18n.ts +34 -0
- package/src/instance.ts +2 -0
- package/src/mail/mail.ts +50 -11
- package/src/mail/providers.ts +6 -3
- package/src/metadata.ts +11 -3
- package/src/network-acl.ts +126 -0
- package/src/objectstore.ts +3 -0
- package/src/runtime/ai-agents.ts +365 -0
- package/src/runtime/ai-filter.ts +385 -107
- package/src/runtime/assistant.ts +64 -11
- package/src/runtime/context.ts +2 -0
- package/src/runtime/engine.ts +3 -1
- package/src/runtime/ir-phrases.ts +812 -0
- package/src/runtime/ir-plan.ts +449 -0
- package/src/runtime/items.ts +1 -1
- package/src/runtime/regions.ts +6 -2
- package/src/runtime/render.ts +5 -3
- package/src/runtime/report.ts +1 -1
- package/src/runtime/styles.ts +54 -12
- package/src/runtime/tasks.ts +63 -12
- package/src/task-events.ts +232 -0
- package/src/team-samples.ts +74 -0
- package/src/team.ts +342 -0
- package/src/webclient.ts +45 -4
- package/src/webrequests.ts +5 -0
- package/src/websources.ts +30 -12
- package/src/workflow.ts +135 -18
- package/src/workspace-export.ts +16 -1
|
@@ -28,13 +28,14 @@ simply closes the dialog.
|
|
|
28
28
|
### Menu buttons and badges
|
|
29
29
|
|
|
30
30
|
A button with action `menu` opens a dropdown (an HTML `<details>` element, so it works without
|
|
31
|
-
JavaScript). `menu` is a JSON array of at most 20 entries; each is
|
|
32
|
-
the application
|
|
31
|
+
JavaScript). `menu` is a JSON array of at most 20 entries; each is a **link** to a page of
|
|
32
|
+
the application, a **submit request** or an **action** that runs dynamic actions:
|
|
33
33
|
|
|
34
34
|
```json
|
|
35
35
|
[{"label": "Leave requests", "page": 6, "icon": "list"},
|
|
36
36
|
{"label": "Details", "page": 3, "items": {"P3_EMPNO": "&P22_EMPNO."}},
|
|
37
|
-
{"label": "Start over", "request": "RESET", "confirm": "Clear the form?", "authz": "ADMIN"}
|
|
37
|
+
{"label": "Start over", "request": "RESET", "confirm": "Clear the form?", "authz": "ADMIN"},
|
|
38
|
+
{"label": "Export", "action": "EXPORT", "icon": "download"}]
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
| Key | Meaning |
|
|
@@ -42,6 +43,7 @@ the application or a **submit request**:
|
|
|
42
43
|
| `label` | The entry's text (1–100 characters) |
|
|
43
44
|
| `page`, `items` | A link: the page and the items to set (signed with a checksum, like any link) |
|
|
44
45
|
| `request` | A submit: the page is submitted with this request, as if a button of that name was pressed. Validations, processes and branches with `when_button` see it |
|
|
46
|
+
| `action` | Runs the [dynamic actions of event `menu_entry`](07-dynamic-actions.md#menu-entry-chosen) with this name (capitals, digits and `_`) as trigger (APEX: Menu entry chosen) |
|
|
45
47
|
| `confirm` | A confirmation question first |
|
|
46
48
|
| `authz` | The entry only exists for users who pass this authorization scheme |
|
|
47
49
|
| `icon` | An [icon](09-reference.md#icons) name |
|
|
@@ -779,7 +781,12 @@ Microsoft retires basic authentication (a password) for SMTP in Exchange Online:
|
|
|
779
781
|
or the Graph provider. Secrets are stored encrypted with `PGKILN_SECRET_KEY`, are write-only in the
|
|
780
782
|
builder, never exported and never readable by applications. Each service lists the **From
|
|
781
783
|
addresses** it may send as (an address, or `@example.com` for a whole domain); an administrator
|
|
782
|
-
chooses each application's service, its From address and an optional **limit per day**.
|
|
784
|
+
chooses each application's service, its From address and an optional **limit per day**. A
|
|
785
|
+
workspace's administrators can also set up the [workspace's own mail
|
|
786
|
+
service](03-builder.md#a-workspaces-own-mail-service) (sprint 118): the workspace's applications that
|
|
787
|
+
an administrator gave no service send through it, with its default From address. Administration →
|
|
788
|
+
[Mail queue](03-builder.md#mail-queue) shows the delivery per hour and per application, the slowest
|
|
789
|
+
provider calls and the failures with the provider's answer, with Retry and Purge.
|
|
783
790
|
|
|
784
791
|
**How mail is sent.** `meta.send_mail` and the others only *queue* the mail, in the caller's
|
|
785
792
|
transaction: a transaction that rolls back sends nothing. After the commit, the server sends it at
|
|
@@ -852,6 +859,9 @@ application ask someone to approve, reject or complete something, and act on the
|
|
|
852
859
|
| When overdue, Maximum renewals, Escalation role | The expiration policy ([below](#overdue-tasks-expire-renew-escalate)); empty: overdue tasks stay open |
|
|
853
860
|
| Details page, Details item | The subject links to this page, setting the item to the record key |
|
|
854
861
|
| On completion (SQL) | Runs as the application's role when the task is approved, rejected or completed (below) |
|
|
862
|
+
| Outcomes | Custom outcomes instead of Approve / Reject ([below](#custom-outcomes)) |
|
|
863
|
+
| Events, When the completion SQL fails | Actions when a task is completed, expires or its completion SQL fails ([below](#task-events-and-notifications)) |
|
|
864
|
+
| Notifications | E-mail templates pgkiln sends when a task is created, completed or expires |
|
|
855
865
|
|
|
856
866
|
**2. Application SQL creates tasks**, for example in the process that saves a request or in a
|
|
857
867
|
trigger:
|
|
@@ -903,6 +913,62 @@ When a record is decided somewhere else (for example with buttons on its own pag
|
|
|
903
913
|
open tasks so they don't linger: `select meta.close_tasks('EXPENSE_APPROVAL', :P5_ID::text,
|
|
904
914
|
'approved')` (no completion SQL runs).
|
|
905
915
|
|
|
916
|
+
<a id="custom-outcomes"></a>**Custom outcomes** (APEX 26.2: task outcomes). A task definition may list its own outcomes
|
|
917
|
+
instead of Approve / Reject (or Complete):
|
|
918
|
+
|
|
919
|
+
```json
|
|
920
|
+
[{"name": "YES", "label": "Yes", "colour": "success"},
|
|
921
|
+
{"name": "NO", "label": "No", "colour": "danger"},
|
|
922
|
+
{"name": "UNDECIDED", "label": "Undecided", "colour": "warning"}]
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
The task list then shows one button per outcome, with its label and colour (`default`, `primary`, `success`,
|
|
926
|
+
`danger` or `warning`: a fixed list, so a definition can't add other classes). The outcome is stored in lower case
|
|
927
|
+
(`meta.tasks.outcome = 'yes'`), `:OUTCOME` in the completion SQL is the name (`YES`), and the history shows the label.
|
|
928
|
+
A task ends only with an outcome of its definition: `meta.complete_task` refuses any other (also Approve / Reject
|
|
929
|
+
on a definition with its own outcomes), and so does `meta.close_tasks(name, detail_pk, outcome)`. A
|
|
930
|
+
[workflow](#workflows) branches on them in a task step's `next` (`{"yes": "PAY", "no": "END"}`) or a switch case
|
|
931
|
+
`{"outcome": "UNDECIDED", "next": "ASK_AGAIN"}`. Names are letters, digits and underscores (at most 30, at most 10
|
|
932
|
+
outcomes); `cancelled`, `expired` and `default` are reserved.
|
|
933
|
+
|
|
934
|
+
<a id="task-events-and-notifications"></a>**Task events and notifications** (APEX 26.2: Human Task events). A
|
|
935
|
+
definition's *Events* are actions on what happens to its tasks:
|
|
936
|
+
|
|
937
|
+
```json
|
|
938
|
+
[{"on": "complete", "type": "sql", "code": "select expenses.log_decision(:DETAIL_PK::int, :OUTCOME, :APPROVER)"},
|
|
939
|
+
{"on": "expire", "type": "mail", "to": "select email from staff where role = 'manager'", "template": "TASK_EXPIRED"},
|
|
940
|
+
{"on": "fail", "type": "workflow", "workflow": "FIX_REQUEST", "vars": {"REASON": "&ERROR."}}]
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
| `on` | When |
|
|
944
|
+
|---|---|
|
|
945
|
+
| `complete` | The task was completed, with any outcome (also by `meta.close_tasks` with an outcome) |
|
|
946
|
+
| `expire` | The task expired ([below](#overdue-tasks-expire-renew-escalate)) |
|
|
947
|
+
| `fail` | The definition's completion SQL raised an error |
|
|
948
|
+
|
|
949
|
+
| `type` | Does | Fields |
|
|
950
|
+
|---|---|---|
|
|
951
|
+
| `sql` | Runs statements as the application's role | `code` |
|
|
952
|
+
| `mail` | Queues an e-mail through [`meta.send_mail_template`](#e-mail-templates) or [`meta.send_mail`](#sending-e-mail) | `to` (a SELECT returning addresses, or addresses with `&NAME.`), `template`, or `subject` and `body` with `#NAME#` placeholders |
|
|
953
|
+
| `workflow` | Starts a workflow with the task's record key and parameters | `workflow`, `vars` (variable → text, `&NAME.` allowed) |
|
|
954
|
+
|
|
955
|
+
Binds (and `#NAME#` placeholders): the task's parameters, `:TASK_ID`, `:DETAIL_PK`, `:SUBJECT`, `:OUTCOME`,
|
|
956
|
+
`:APPROVER`, `:INITIATOR`, `:EVENT`, and for `fail` `:ERROR` and `:COMMENT`. *Complete* and *expire* events run
|
|
957
|
+
right after the commit: a trigger queues them (`meta.task_hook`, closed to applications) and the server runs each
|
|
958
|
+
in a transaction of its own, as the application's role with the person who completed the task (or the initiator) as
|
|
959
|
+
`meta.app_user()` and the task's tenant; one that fails is tried three times and keeps its error on the
|
|
960
|
+
`meta.task_hook` row. *Fail* events run in the request that completed the task: the completion SQL's effect is
|
|
961
|
+
undone, the failure is in the task's history, the actions run, and **When the completion SQL fails** says what
|
|
962
|
+
happens to the task: `reopen` (the default) undoes the decision so the task stays open, `complete` keeps it completed
|
|
963
|
+
without the SQL's effect. The user sees the error either way. Without fail events or a recovery, an error undoes the
|
|
964
|
+
decision as before.
|
|
965
|
+
|
|
966
|
+
**Notifications** are e-mails pgkiln sends itself, from the application's
|
|
967
|
+
[e-mail templates](#e-mail-templates): `{"create": "TASK_CREATED", "complete": "TASK_DONE", "expire": "TASK_EXPIRED"}`.
|
|
968
|
+
*create* goes to the potential owners (the named users, the actual owner and the application's users with one of
|
|
969
|
+
the owner roles), *complete* to the initiator, *expire* to both, at the e-mail address of their account. The
|
|
970
|
+
placeholders are those of the events. They need a mail service, like `meta.send_mail`.
|
|
971
|
+
|
|
906
972
|
#### Overdue tasks: expire, renew, escalate
|
|
907
973
|
|
|
908
974
|
A task definition with *Due in* can say what happens to a task that is still open after its due date (APEX:
|
|
@@ -930,9 +996,9 @@ workflow whose task is cancelled.
|
|
|
930
996
|
| `meta.task_events` | View: the history and comments of those tasks |
|
|
931
997
|
|
|
932
998
|
The task tables themselves are closed to the application's role: rights are checked by these
|
|
933
|
-
functions and views, for every call. To tell owners by e-mail,
|
|
934
|
-
[`meta.send_mail`](#sending-e-mail) from the task
|
|
935
|
-
reminds owners of overdue tasks; or put a task list on the home page.
|
|
999
|
+
functions and views, for every call. To tell owners by e-mail, use the definition's
|
|
1000
|
+
[notifications](#task-events-and-notifications), call [`meta.send_mail`](#sending-e-mail) from the task
|
|
1001
|
+
definition's SQL or from an automation that reminds owners of overdue tasks; or put a task list on the home page.
|
|
936
1002
|
|
|
937
1003
|
**Example:** in the HR sample application, a leave request creates a `LEAVE_APPROVAL` task for the
|
|
938
1004
|
employee's manager; *My tasks* (page 14) approves or rejects it through `hr.decide_leave`, the same
|
|
@@ -954,9 +1020,15 @@ steps; the builder checks them and draws the flow.
|
|
|
954
1020
|
| `invoke_api` | Calls a REST data source or a URL on the server; values of the response become variables ([below](#invoke-api-steps)) | `source` + `params`, or `url`, `method`, `credential`, `body`; `variables`, `status_variable`, `response_variable`, `timeout`, `next` |
|
|
955
1021
|
| `parallel` | Starts a branch at each of its steps; they run side by side | `branches` (the first step of each, at least two), `join` |
|
|
956
1022
|
| `join` | Where the branches of a `parallel` step meet | `wait_for` (`all`, the default, or `any`), `next` |
|
|
1023
|
+
| `send_mail` | Queues an e-mail ([below](#send-e-mail-steps)) | `to`, `template` (or `subject` and `body`), `cc`, `next` |
|
|
1024
|
+
| `draft` | A placeholder while modelling: only in a development version ([below](#draft-steps)) | `note`, `next` |
|
|
957
1025
|
| `end` | Ends the workflow | |
|
|
958
1026
|
|
|
959
1027
|
`next` is optional: the following step in the list (after the last one, the workflow is complete).
|
|
1028
|
+
A task step's `next` per outcome names the outcomes of its task definition, [custom ones](#custom-outcomes) too,
|
|
1029
|
+
plus `cancelled`, `expired` and `default`. A `switch` case is either `{"when": "SQL condition", "next": …}` or
|
|
1030
|
+
`{"outcome": "YES", "next": …}`, which compares the outcome of the last task (`:TASK_OUTCOME`); the builder checks
|
|
1031
|
+
that a task of the workflow can end with it.
|
|
960
1032
|
|
|
961
1033
|
```json
|
|
962
1034
|
[{"name": "CHECK", "type": "switch", "cases": [{"when": ":AMOUNT::numeric > 1000", "next": "DIRECTOR"}], "otherwise": "MANAGER"},
|
|
@@ -976,7 +1048,7 @@ select meta.start_workflow('EXPENSE', :P5_ID, jsonb_build_object('AMOUNT', :P5_A
|
|
|
976
1048
|
|
|
977
1049
|
The variables (upper case) are binds in every step, with `:DETAIL_PK`, `:WORKFLOW_ID` and
|
|
978
1050
|
`:INITIATOR`; after a task step also `:TASK_OUTCOME` (`APPROVED`, `REJECTED`, `COMPLETED`,
|
|
979
|
-
`CANCELLED`) and `:TASK_APPROVER`. The title may use `&VAR.`.
|
|
1051
|
+
`CANCELLED`, `EXPIRED` or a custom outcome such as `YES`) and `:TASK_APPROVER`. The title may use `&VAR.`.
|
|
980
1052
|
|
|
981
1053
|
**How it runs.** The pgkiln server runs workflows: right after they start or a task of theirs
|
|
982
1054
|
ends (`NOTIFY`), and it checks for waits that are over every few seconds (`WORKFLOW_INTERVAL_S`,
|
|
@@ -1008,6 +1080,29 @@ background runs with the tenant of the session that queued it. Filter your own t
|
|
|
1008
1080
|
(`where tenant_id = meta.tenant_id()`, or a row level security policy). Use row level security in
|
|
1009
1081
|
the tenant's tables as well: the tenant is a value the application sets, not a database boundary.
|
|
1010
1082
|
|
|
1083
|
+
### Send e-mail steps
|
|
1084
|
+
|
|
1085
|
+
A `send_mail` step (APEX 26.1: the *Send E-Mail* activity) queues an e-mail through the application's mail service,
|
|
1086
|
+
as the application's role, like [`meta.send_mail`](#sending-e-mail) in a `sql` step:
|
|
1087
|
+
|
|
1088
|
+
```json
|
|
1089
|
+
{"name": "TELL", "type": "send_mail", "to": "select email from staff where id = :DETAIL_PK::int", "template": "REQUEST_DONE"}
|
|
1090
|
+
{"name": "NOTE", "type": "send_mail", "to": "desk@example.com, &MANAGER_EMAIL.", "subject": "Request #TITLE# done", "body": "Decided by #TASK_APPROVER#."}
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
`to` is a SELECT returning addresses, or addresses with `&VAR.` substitutions; `cc` likewise addresses. With a
|
|
1094
|
+
`template` ([e-mail templates](#e-mail-templates)), or a `subject` and a plain-text `body`, the placeholders `#NAME#`
|
|
1095
|
+
are the variables, `DETAIL_PK`, `WORKFLOW_ID` and `INITIATOR`. No recipient: the step goes on (the history says
|
|
1096
|
+
so). A mail that can't be queued (no mail service, an address that isn't one, the daily limit) faults the step.
|
|
1097
|
+
The history records the mail's id; it is sent after the step's transaction commits.
|
|
1098
|
+
|
|
1099
|
+
<a id="draft-steps"></a>**Draft steps** (APEX 26.2: Draft activities). A step of type `draft` stands for something
|
|
1100
|
+
not modelled yet (`{"name": "BUDGET_CHECK", "type": "draft", "note": "check the budget", "next": "APPROVE"}`).
|
|
1101
|
+
It may be in a development version only: the builder shows a warning and doesn't offer **Activate** while a version
|
|
1102
|
+
has one, `meta.activate_workflow_version` refuses it, and the database refuses draft steps in a definition's active
|
|
1103
|
+
steps (so a new definition can't have them either: create it, then a new version to model with drafts). The diagram
|
|
1104
|
+
draws them dashed. An instance can never reach one; if it did (steps changed with SQL), the step faults.
|
|
1105
|
+
|
|
1011
1106
|
### Invoke API steps
|
|
1012
1107
|
|
|
1013
1108
|
An `invoke_api` step (APEX: the *Invoke API* activity) calls a web service: a
|
|
@@ -1086,6 +1181,28 @@ steps with problems), and the active one becomes inactive. **Discard** throws a
|
|
|
1086
1181
|
away. The Versions table shows each version with its instance count and diagram; the Instances
|
|
1087
1182
|
table shows which version each instance runs and where it is.
|
|
1088
1183
|
|
|
1184
|
+
<a id="migrating-instances"></a>**Migrating running instances** (APEX 26.2: in-flight workflow migration). Running
|
|
1185
|
+
instances (active, waiting or faulted) of an older version can move to a newer one: the active version or an inactive
|
|
1186
|
+
version activated after theirs. Under the definition, **Migrate instances…** (shown
|
|
1187
|
+
while there are such instances) opens a page where you choose the version and the instances, and map each step an
|
|
1188
|
+
instance is at to a step of the new version (a step of the same name is proposed). **Preview** shows per instance
|
|
1189
|
+
where it goes or why it can't; **Migrate** then moves them all in one transaction, with their variables, the task
|
|
1190
|
+
they wait for and their wait, logged in each instance's history (*migrated*, `version 1 → 2: APPROVE → APPROVE
|
|
1191
|
+
(by admin)`) and in `meta.workflow_migration`. Nothing moves when one of them can't:
|
|
1192
|
+
|
|
1193
|
+
- a step without a mapping, or mapped to a step the version doesn't have (or a draft);
|
|
1194
|
+
- an instance waiting for a task must go to a task step that creates a task of the same definition;
|
|
1195
|
+
one at a `wait` step to a `wait` step (its wait goes on), one at a join to a join;
|
|
1196
|
+
- an instance inside parallel branches, or calling a web service at that moment: try again later.
|
|
1197
|
+
|
|
1198
|
+
The database function is `meta.migrate_workflows(definition_id, version, ids, mapping, apply, by)` (the owner's,
|
|
1199
|
+
for the builder and scripts; not for applications).
|
|
1200
|
+
|
|
1201
|
+
<a id="diagram-export"></a>**The diagram as a file.** Under the diagram, **Download SVG** downloads the whole model
|
|
1202
|
+
of the version shown (`/builder/apps/<app>/shared/workflow_definition/<id>/diagram.svg?v=<version>`), a standalone
|
|
1203
|
+
SVG with its own colours; **Download PNG** draws that SVG in the browser (with JavaScript; without it the SVG link
|
|
1204
|
+
remains). Step names and labels are escaped, so a definition can't put markup into the file.
|
|
1205
|
+
|
|
1089
1206
|
**The console.** A region of type **`workflows`** lists the workflows the user started (or, with
|
|
1090
1207
|
`"context": "admin"`, those they administer), with their state, the version, the current steps (several with parallel branches), a diagram
|
|
1091
1208
|
and the history. The initiator and administrators can **terminate** a workflow (its open tasks and
|
|
@@ -1102,7 +1219,10 @@ branches are cancelled); administrators can retry a faulted one.
|
|
|
1102
1219
|
created. Its version 2 (part 18) prepares the workplace (the manager's task) and, in a parallel
|
|
1103
1220
|
branch, the access that employees with a salary of 2500 or more need (a switch and an
|
|
1104
1221
|
administrator's task); when both branches are done, the manager is notified (SQL). Version 1, which
|
|
1105
|
-
did this one after the other, is inactive. *My tasks* (page 14) shows the workflows. Part
|
|
1222
|
+
did this one after the other, is inactive. *My tasks* (page 14) shows the workflows. Part 67 adds `SALARY_CHANGE`: a
|
|
1223
|
+
`SALARY_DECISION` task with the outcomes Yes, No and Undecided (the owners get the `SALARY_TASK` e-mail, a complete
|
|
1224
|
+
event notifies the initiator), a switch on the outcome (Undecided asks again), SQL that applies the salary and, in
|
|
1225
|
+
version 2, a `send_mail` step to the employee; version 3 is in development with a draft step. Part 34 adds
|
|
1106
1226
|
`DEPARTMENT_CHECK`, started by *Check in a workflow* on page 23: an `invoke_api` step reads the
|
|
1107
1227
|
department from the example's own REST API (the `DEPARTMENT` source of part 23, so the server must
|
|
1108
1228
|
allow `127.0.0.1:3100`), a `switch` on `HTTP_STATUS`, and SQL that notifies the initiator.
|
|
@@ -37,6 +37,7 @@ your own from a static application file.
|
|
|
37
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
38
|
| `before_submit` | The page is about to be submitted; a declined `confirm` keeps the page | – |
|
|
39
39
|
| `custom` | JavaScript raised the event: `pgkiln.trigger('ORDER_SAVED', value)` (APEX: custom events, `apex.event.trigger`) | Event names (case-insensitive) |
|
|
40
|
+
| `menu_entry` | An entry `{"label": …, "action": "NAME"}` of a [menu button](06-processing.md#menu-buttons-and-badges) was chosen (APEX: Menu entry chosen); the condition sees the entry's name ([below](#menu-entry-chosen)) | Entry names, or menu button names (any of their entries) |
|
|
40
41
|
|
|
41
42
|
**The actions of one event and trigger run one after the other, in sequence order** (APEX: the true
|
|
42
43
|
actions of a dynamic action), each waiting for the one before (a server action for its answer). A
|
|
@@ -79,11 +80,44 @@ actions of a dynamic action), each waiting for the one before (a server action f
|
|
|
79
80
|
| `print` | browser | The browser's print dialog |
|
|
80
81
|
| `current_position` | browser | The device's position as `latitude,longitude` (5 decimals) into the first affected item |
|
|
81
82
|
| `share` | browser | The device's share sheet with the page's title, `message` and address; where there is none, the address is copied |
|
|
83
|
+
| `wait_popup` | browser | Shows a modal "please wait" popup with `message` (default *Please wait…*) until `remove_wait_popup` or the next page (APEX: Show Wait Popup); [below](#wait-popup) |
|
|
84
|
+
| `remove_wait_popup` | browser | Removes the wait popup (APEX: Remove Wait Popup) |
|
|
82
85
|
|
|
83
86
|
Server-side actions first store `items_to_submit` in session state, run as the application's
|
|
84
87
|
database role like everything else, and check the page's and the dynamic action's authorization.
|
|
85
88
|
The SQL itself never reaches the browser.
|
|
86
89
|
|
|
90
|
+
## Wait popup
|
|
91
|
+
|
|
92
|
+
`wait_popup` covers the page with a small dialog (a spinner and `message`) while something slow
|
|
93
|
+
runs, so the user doesn't click twice; `remove_wait_popup` takes it away. Because the actions of
|
|
94
|
+
one event run in sequence, each waiting for the one before, two patterns cover most cases:
|
|
95
|
+
|
|
96
|
+
| Sequence | Event | Action | |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| 10 | `click` on `RECALC` (a submit button) | `wait_popup` | *Recalculating…*: the popup stays until the next page arrives |
|
|
99
|
+
| 10 | `click` on `CHECK` | `wait_popup` | *Checking…* |
|
|
100
|
+
| 20 | `click` on `CHECK` | `execute_sql` | the Ajax call (also `set_value`, `refresh_region`, `download`, a plug-in) |
|
|
101
|
+
| 30 | `click` on `CHECK` | `remove_wait_popup` | |
|
|
102
|
+
|
|
103
|
+
`before_submit` → `wait_popup` shows it for every submit of the page. A submit the browser refuses
|
|
104
|
+
(a required field is empty) removes it again, and so does going back to the page in the browser.
|
|
105
|
+
The popup is a modal `<dialog>`: the page below can't be clicked or focused, Escape doesn't close
|
|
106
|
+
it, its text is a status message that screen readers read out, and the page's `<main>` is
|
|
107
|
+
`aria-busy` while it shows. It follows the theme (light and dark) and needs no inline styles or
|
|
108
|
+
scripts. From JavaScript: `const done = pgkiln.waitPopup('Saving…'); … done();` (or
|
|
109
|
+
`pgkiln.removeWaitPopup()`).
|
|
110
|
+
|
|
111
|
+
## Menu entry chosen
|
|
112
|
+
|
|
113
|
+
A [menu button](06-processing.md#menu-buttons-and-badges)'s entry can run dynamic actions instead
|
|
114
|
+
of linking or submitting: `{"label": "Export", "action": "EXPORT"}`. Dynamic actions with event
|
|
115
|
+
`menu_entry` and trigger `EXPORT` run when it is chosen (the menu closes first); a trigger with the
|
|
116
|
+
menu button's name runs for every action entry of that menu, and a client-side condition tests the
|
|
117
|
+
entry's name (`equals` `EXPORT`). Without JavaScript such an entry does nothing, like a button with
|
|
118
|
+
action `da`. The HR example's page 47 has an *Actions* menu whose *Count employees* entry shows a
|
|
119
|
+
wait popup around an Ajax call.
|
|
120
|
+
|
|
87
121
|
## Ajax callbacks
|
|
88
122
|
|
|
89
123
|
A page process with point **ajax** (APEX: an On Demand process) runs only when JavaScript calls it by
|
|
@@ -540,6 +540,61 @@ quoted with `escapeIdentifier`, every value is checked (dates, numbers, schema n
|
|
|
540
540
|
The page is for administrators only (others get 403), every change needs the CSRF token, and pgkiln's own roles can't
|
|
541
541
|
be changed, dropped or granted from it, so the page can't take pgkiln's own connections away.
|
|
542
542
|
|
|
543
|
+
## Data security per table: rows, columns and values
|
|
544
|
+
|
|
545
|
+
(sprint 119) PostgreSQL has row level security but no column masking, so pgkiln puts a table's security in three
|
|
546
|
+
layers, all ordinary database objects, and shows them on one page: **SQL Workshop → Object Browser → a table →
|
|
547
|
+
Security** (also *Security…* in the table's ⋯ menu). It is pgkiln's answer to APEX 26.2's Deep Data Security.
|
|
548
|
+
|
|
549
|
+
- **Rows**: the table's row level security policies (above), listed with a link to Table Properties to switch row
|
|
550
|
+
level security on and to *Add policy*.
|
|
551
|
+
- **Columns**: a matrix of **database roles × columns** with *read* and *update* per cell. The rows are the roles
|
|
552
|
+
that have privileges on the table and the database roles of the workspace's applications (with the applications
|
|
553
|
+
that run as them). Apply writes `GRANT` / `REVOKE SELECT (c, …)` and `UPDATE (c, …)`. A column a role may not read
|
|
554
|
+
is refused by PostgreSQL ("permission denied") whoever sends the query. The table's owner, superusers and roles that
|
|
555
|
+
bypass row level security are listed apart: column privileges can't restrict them.
|
|
556
|
+
- **Values**: masked columns per **application role** (`meta.has_role()` of the signed-in user). A column's mask is
|
|
557
|
+
*empty (null)*, `***` or `***` plus the last four characters; tick the application roles that see the real value,
|
|
558
|
+
and *Everyone else* for users with none of the listed roles. The masks live in a **masking view** next to the table
|
|
559
|
+
(default name `<table>_masked`):
|
|
560
|
+
|
|
561
|
+
```sql
|
|
562
|
+
create view sales.orders_masked with (security_barrier) as
|
|
563
|
+
select t.id, t.customer,
|
|
564
|
+
case when (select meta.has_role('manager')) then t.amount end as amount,
|
|
565
|
+
case when false then t.iban::text else '***' || right(t.iban::text, 4) end as iban
|
|
566
|
+
from sales.orders t;
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The view is owned by a role per schema, `<schema>_masking` (nologin), which pgkiln creates the first time. That
|
|
570
|
+
role is no owner, superuser or role that bypasses row level security (Apply checks it), so the table's policies
|
|
571
|
+
still apply inside the view; and it is a member of the database roles that read the table, so policies written
|
|
572
|
+
`to <role>` apply as well. The configuration is kept in the view's comment, so it travels with `pg_dump`,
|
|
573
|
+
Generate DDL and the change log; there is no `meta` table for it.
|
|
574
|
+
|
|
575
|
+
**Preview SQL** shows the statements, **Apply** runs them in one transaction (and records a change set in the
|
|
576
|
+
Change Log), **Open in the worksheet** puts them there. Nothing is changed when one statement fails.
|
|
577
|
+
|
|
578
|
+
**The mask holds only where the view is read.** As long as a role may still read the raw column on the table, its
|
|
579
|
+
queries on the table see the value; the preview warns about each such column. Point the application's pages,
|
|
580
|
+
REST handlers and AI tools at the view, then untick *read* of the masked columns for the role, and the value is
|
|
581
|
+
masked everywhere that role connects.
|
|
582
|
+
|
|
583
|
+
Every client meets the same rules because they all run as the application's database role with the same identity:
|
|
584
|
+
|
|
585
|
+
| Client | Runs as | `meta.app_user()` / `meta.has_role()` from |
|
|
586
|
+
|---|---|---|
|
|
587
|
+
| Pages, processes, dynamic actions, AI assistant and AI agent tools | the application's role (`appTx`) | the session |
|
|
588
|
+
| REST module handlers, AutoREST | the application's role | the bearer token (its user, the user's roles) |
|
|
589
|
+
| The MCP server's `run_query` with `alias` (and `user`) | the application's role | the user's roles in the application ([chapter 20](20-ai-agents.md#safety)) |
|
|
590
|
+
| psql, other tools | whatever role they log in as | — (only grants and policies apply) |
|
|
591
|
+
|
|
592
|
+
Policies and masks that use `meta.has_role()` can be combined: with a policy that shows a manager the rows of their
|
|
593
|
+
department, and `amount` masked for everyone but `manager`, a clerk reads their own rows with an empty amount.
|
|
594
|
+
Policies written per database role (`to sales_app`) apply to the masking role for every role it is a member of, so
|
|
595
|
+
prefer policies on `meta.app_user()` / `meta.has_role()` on tables with a masking view. The HR example's page 48
|
|
596
|
+
*Salaries (masked)* reads `hr.emp_masked` (`examples/hr/hr_70_data_security.sql`).
|
|
597
|
+
|
|
543
598
|
## Session state protection
|
|
544
599
|
|
|
545
600
|
- **URL items**: on pages with protection *Arguments must have checksum* (the default), item
|
|
@@ -81,7 +81,9 @@ select meta.web_request_source('EXCHANGE', '{"currency": "EUR"}');
|
|
|
81
81
|
must be on `PGKILN_REST_ALLOWED_HOSTS` (unset: no calls), private and loopback addresses are refused
|
|
82
82
|
when the connection is made unless the host is in `PGKILN_REST_PRIVATE_HOSTS`, redirects (at most 3)
|
|
83
83
|
go through the same checks, the response is cut off at `PGKILN_REST_MAX_BYTES` (default 5 MB) and
|
|
84
|
-
the time limit is 1–60 seconds.
|
|
84
|
+
the time limit is 1–60 seconds. A workspace's [network access list](03-builder.md#network-access)
|
|
85
|
+
(sprint 118) narrows the hosts for its applications: a refused request ends with `status` `error`
|
|
86
|
+
and the reason in `message`, and is logged (debug log, Refused calls).
|
|
85
87
|
- **Web credentials** are named, never passed: `p_credential => 'NAME'` signs the request with a
|
|
86
88
|
credential of the current application (basic, bearer, header, OAuth2), and only for the URLs it is
|
|
87
89
|
valid for. The secret is decrypted by the server for the request only and is never stored with it
|
|
@@ -481,9 +483,10 @@ navigation entries and application processes have the same `build_option` column
|
|
|
481
483
|
| `push_key` | The VAPID key pair of an application with [push notifications](17-mobile.md#push-notifications): `public_key`, `private_key` (encrypted with `PGKILN_SECRET_KEY`). Not exported | no |
|
|
482
484
|
| `push_subscription` | Devices with notifications on: `app_id`, `username`, `endpoint` (the push service URL), the device's keys `p256dh` and `auth`, `user_agent`, `created_at`, `last_sent_at`, `failures`. Not exported | no (through `meta.has_push_subscription`) |
|
|
483
485
|
| `push_message` | [Push notifications from SQL](#push-notifications-from-sql): `app_id`, `username`, `title`, `body`, `url`, `tag`, `urgency`, `ttl_s`, `status`, `attempts`, `devices`, `delivered`, `message`, `requested_by`, times. Kept 7 days, not exported | no |
|
|
484
|
-
| `ai_service` | [AI services](06-processing.md#generate-text-with-ai) of the installation: `name`, `provider` (`anthropic`, `openai`), `model`, `effort`, `refusal_fallback`, `max_tokens`, `timeout_s`, `base_url`, `api_key_enc` (encrypted with `PGKILN_SECRET_KEY`, write-only), `enabled`. Not exported | no |
|
|
486
|
+
| `ai_service` | [AI services](06-processing.md#generate-text-with-ai) of the installation: `name`, `provider` (`anthropic`, `openai`, `gemini`, `openai_compatible`: 121), `model`, `effort`, `refusal_fallback`, `max_tokens`, `timeout_s`, `base_url`, `api_key_enc` (encrypted with `PGKILN_SECRET_KEY`, write-only), `enabled`. Not exported | no |
|
|
485
487
|
| `app_ai_service` | Which applications may use which AI service, with daily limits `max_requests` and `max_tokens` (null: no limit). Not exported | no (through `meta.ai_available`) |
|
|
486
488
|
| `ai_usage` | One row per AI request: `at`, `app_id`, `page_no`, `username`, `service`, `provider`, `model`, `source`, `input_tokens`, `output_tokens`, `duration_ms`, `status`, `message` (an error class, never prompt or answer text). Not exported | no |
|
|
489
|
+
| `ai_tool_call` | (121) Every tool call of an [AI assistant or agent](03-builder.md#ai-agents): `at`, `app_id`, `page_no`, `region_id`, `username`, `agent`, `tool`, `type`, `arguments` (the model's, cut to 4 KB), `status` (`ok`, `error`, `refused`), `duration_ms`, `message`. Kept 30 days, not exported | no |
|
|
487
490
|
| `ai_conversation` | [AI assistant](04-pages-and-regions.md#ai_assistant-ai-assistant) conversations: one per session and region (`messages`: the provider's history, `turns`: what the page shows); deleted with the session. Not exported | no |
|
|
488
491
|
| `ai_table_note` | Descriptions of tables and columns for models ([App Builder AI](03-builder.md#app-builder-ai)): `schema_name`, `table_name`, `column_name` (`''` for the table), `note`. Installation data, not exported | no |
|
|
489
492
|
| `blueprint` | Saved [blueprints](03-builder.md#creating-an-application-from-a-blueprint): `name`, `spec` (the JSON), `app_id` (the application last created from it), `created_by`. Builder data, not exported | no |
|
|
@@ -22,10 +22,11 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
|
|
|
22
22
|
| Instance administration: install/upgrade log | Workspace utilities → Installation (administrators) |
|
|
23
23
|
| `APEX_WEB_SERVICE.make_rest_request`, `g_status_code` | `meta.web_request(url, method, body, headers, credential)` or `meta.web_request_source(source, params)`, then `meta.web_response(id)`. **Asynchronous**: the server makes the request after the page process that queued it, or after the commit ([chapter 9](09-reference.md#web-requests-from-sql)) |
|
|
24
24
|
| Workspace → Generative AI services; *Generate Text with AI* process and dynamic action; `APEX_AI.generate` | Workspace utilities → AI services (Claude or OpenAI; administrators); the `ai_generate` process and dynamic action, with structured outputs into items ([chapter 6](06-processing.md#generate-text-with-ai)); `meta.ai_generate(service, prompt, system, schema)` then `meta.ai_result(id)` ([chapter 9](09-reference.md#ai-requests-from-sql)) |
|
|
25
|
-
| AI Assistant region; AI agents with tools; natural language to interactive report
|
|
25
|
+
| AI Assistant region; AI agents with tools; AI interactive reports (natural language to interactive report) | The `ai_assistant` region: a chat with context queries (RAG over your queries) and tools (SQL as the app role with bound arguments, REST data sources) ([chapter 4](04-pages-and-regions.md#ai_assistant-ai-assistant)); `"ai_filter"` on a report: *Ask in your own words*, the request becomes the report's settings as editable chips, with or without an AI service ([chapter 4](04-pages-and-regions.md#ask-in-your-own-words-ai-interactive-reports)) |
|
|
26
26
|
| App Builder / SQL Workshop AI Assistant (create pages, generate and explain SQL); describing tables for AI | The App Builder's AI service: *Create pages with AI* (proposals you confirm), SQL Workshop → AI (SQL from a question, shown not run; explain a query or error), *Describe tables* (`meta.ai_table_note`, optionally `COMMENT ON`) ([chapter 3](03-builder.md#app-builder-ai)) |
|
|
27
27
|
| Blueprints (spec-driven development) | Create → From a blueprint: a JSON blueprint (tables, pages, menu, sample data), optionally drafted by AI, reviewed, then created in one transaction ([chapter 3](03-builder.md#creating-an-application-from-a-blueprint)) |
|
|
28
28
|
| `APEX_DATA_PARSER.parse`, `get_columns` | `meta.parse_data(blob, …)` (rows with `cols` and `data`), `meta.parse_data_columns(blob, …)`; CSV/TSV and JSON, not Excel or XML ([chapter 9](09-reference.md#parsing-files-in-sql)) |
|
|
29
|
+
| Team Development (issues, labels, milestones, templates, feedback) | Builder → Team Development (`/builder/team`): issues per workspace with labels in groups, milestones, assignees, application and page links, Markdown comments with @mentions shown in the builder, history, saved views and a board; a set-up wizard with sample labels, templates and milestones; feedback and developer comments become issues. No e-mail and no GitHub synchronisation ([chapter 3](03-builder.md#team-development)) |
|
|
29
30
|
| Workspace users (APEX accounts), Application Access Control | `meta.account` (Builder → Users), `meta.app_access` (Access control) |
|
|
30
31
|
| Page access protection "Arguments must have checksum" | `protection = 'checksum'` (the default) |
|
|
31
32
|
| Automatic row processing (DML) | Process type `form_dml` |
|
|
@@ -64,6 +65,7 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
|
|
|
64
65
|
| Automatic Time Zone | Settings → Globalization → Time zone and Automatic time zone; My account → Time zone ([chapter 14](14-globalization.md#time-zones)) |
|
|
65
66
|
| Universal Theme's default style (26.1: *Iris*) | Base style *Iris* (the default for new applications), *Lavender* or *Standard* ([chapter 14](14-globalization.md#base-styles-iris-lavender-and-standard)) |
|
|
66
67
|
| Theme styles, *Enable End Users to Choose Theme Style* | Theme style (automatic/light/dark) and *Users may choose light or dark*; Theme Roller style variants with *Users may choose a style* ([chapter 14](14-globalization.md#style-variants-theme-roller)) |
|
|
68
|
+
| Universal Theme 26.2: *Pillar Light* / *Pillar Dark*, Theme Roller's *Header Pillar Strip*, `u-color-*-subtle` | Theme Roller → *Navigation appearance* and *Header strip colour* (per application or style), the same utility classes ([chapter 14](14-globalization.md#navigation-appearances-pillar-light-and-pillar-dark)) |
|
|
67
69
|
| Template Options (regions, buttons) | *Template options* in the Page Designer: CSS classes from a fixed list ([chapter 4](04-pages-and-regions.md#template-options)) |
|
|
68
70
|
| ORDS | Not needed to serve apps; REST APIs with PostgREST, see [below](#ords-and-postgrest) |
|
|
69
71
|
| Export `f123.sql` / APEXlang | `meta.export_app('alias')` (JSON) |
|