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.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/SECURITY.md +10 -1
  3. package/db/migrations/121_ai_agents.sql +94 -0
  4. package/db/migrations/122_team_development.sql +186 -0
  5. package/db/migrations/123_workflow_outcomes.sql +407 -0
  6. package/db/migrations/124_component_details.sql +51 -0
  7. package/db/migrations/125_app_change.sql +23 -0
  8. package/db/migrations/126_mail_queue_network_access.sql +119 -0
  9. package/docs/apex-feature-parity.md +50 -19
  10. package/docs/guide/01-installation.md +8 -4
  11. package/docs/guide/03-builder.md +197 -10
  12. package/docs/guide/04-pages-and-regions.md +71 -21
  13. package/docs/guide/05-items.md +7 -1
  14. package/docs/guide/06-processing.md +129 -9
  15. package/docs/guide/07-dynamic-actions.md +34 -0
  16. package/docs/guide/08-security.md +55 -0
  17. package/docs/guide/09-reference.md +5 -2
  18. package/docs/guide/11-from-apex.md +3 -1
  19. package/docs/guide/12-development.md +87 -16
  20. package/docs/guide/14-globalization.md +49 -1
  21. package/docs/guide/18-cli.md +21 -1
  22. package/docs/guide/19-rest-data-sources.md +12 -0
  23. package/docs/guide/20-ai-agents.md +12 -4
  24. package/examples/hr/README.md +2 -0
  25. package/examples/hr/hr_64_ai_agent.sql +97 -0
  26. package/examples/hr/hr_65_ask_reports.sql +21 -0
  27. package/examples/hr/hr_66_team_development.sql +58 -0
  28. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  29. package/examples/hr/hr_68_component_details.sql +96 -0
  30. package/examples/hr/hr_70_data_security.sql +79 -0
  31. package/package.json +1 -1
  32. package/public/app.css +135 -21
  33. package/public/app.js +134 -20
  34. package/public/builder.css +133 -1
  35. package/public/builder.js +51 -0
  36. package/src/ai/chat.ts +18 -18
  37. package/src/ai/service.ts +35 -11
  38. package/src/ai/types.ts +7 -2
  39. package/src/app.ts +15 -0
  40. package/src/builder/advisor.ts +6 -3
  41. package/src/builder/ai-agents.ts +372 -0
  42. package/src/builder/ai-builder.ts +3 -3
  43. package/src/builder/ai.ts +13 -9
  44. package/src/builder/app-updates.ts +55 -0
  45. package/src/builder/assistant.ts +22 -7
  46. package/src/builder/components.ts +38 -9
  47. package/src/builder/data-security.ts +182 -0
  48. package/src/builder/designer.ts +8 -3
  49. package/src/builder/feedback.ts +6 -1
  50. package/src/builder/home.ts +16 -5
  51. package/src/builder/locks.ts +7 -1
  52. package/src/builder/mail-queue.ts +212 -0
  53. package/src/builder/mail.ts +72 -45
  54. package/src/builder/network-access.ts +140 -0
  55. package/src/builder/object-actions.ts +1 -0
  56. package/src/builder/object-browser.ts +1 -1
  57. package/src/builder/region-help.ts +2 -2
  58. package/src/builder/rest-catalogs.ts +19 -4
  59. package/src/builder/routes.ts +4 -1
  60. package/src/builder/shared.ts +1 -1
  61. package/src/builder/sql.ts +2 -0
  62. package/src/builder/team.ts +991 -0
  63. package/src/builder/themeroller.ts +47 -6
  64. package/src/builder/ui.ts +7 -1
  65. package/src/builder/websources.ts +1 -1
  66. package/src/builder/workflow-migrate.ts +210 -0
  67. package/src/builder/workflows.ts +29 -6
  68. package/src/builder/workspace-home.ts +2 -0
  69. package/src/builder/workspace-mail.ts +121 -0
  70. package/src/builder/workspace-transfer.ts +1 -1
  71. package/src/builder/workspaces.ts +1 -0
  72. package/src/cli/main.ts +25 -3
  73. package/src/cli/mcp.ts +40 -4
  74. package/src/cli/only.ts +52 -0
  75. package/src/cli/replace.ts +11 -1
  76. package/src/datasec.ts +343 -0
  77. package/src/i18n/ar.ts +17 -0
  78. package/src/i18n/cs.ts +17 -0
  79. package/src/i18n/da.ts +17 -0
  80. package/src/i18n/de.ts +17 -0
  81. package/src/i18n/el.ts +17 -0
  82. package/src/i18n/es.ts +17 -0
  83. package/src/i18n/fi.ts +17 -0
  84. package/src/i18n/fr.ts +17 -0
  85. package/src/i18n/he.ts +17 -0
  86. package/src/i18n/it.ts +17 -0
  87. package/src/i18n/ja.ts +17 -0
  88. package/src/i18n/ko.ts +17 -0
  89. package/src/i18n/nb.ts +17 -0
  90. package/src/i18n/pl.ts +17 -0
  91. package/src/i18n/pt.ts +17 -0
  92. package/src/i18n/ru.ts +17 -0
  93. package/src/i18n/sv.ts +17 -0
  94. package/src/i18n/tr.ts +17 -0
  95. package/src/i18n/uk.ts +17 -0
  96. package/src/i18n/zh.ts +17 -0
  97. package/src/i18n.ts +34 -0
  98. package/src/instance.ts +2 -0
  99. package/src/mail/mail.ts +50 -11
  100. package/src/mail/providers.ts +6 -3
  101. package/src/metadata.ts +11 -3
  102. package/src/network-acl.ts +126 -0
  103. package/src/objectstore.ts +3 -0
  104. package/src/runtime/ai-agents.ts +365 -0
  105. package/src/runtime/ai-filter.ts +385 -107
  106. package/src/runtime/assistant.ts +64 -11
  107. package/src/runtime/context.ts +2 -0
  108. package/src/runtime/engine.ts +3 -1
  109. package/src/runtime/ir-phrases.ts +812 -0
  110. package/src/runtime/ir-plan.ts +449 -0
  111. package/src/runtime/items.ts +1 -1
  112. package/src/runtime/regions.ts +6 -2
  113. package/src/runtime/render.ts +5 -3
  114. package/src/runtime/report.ts +1 -1
  115. package/src/runtime/styles.ts +54 -12
  116. package/src/runtime/tasks.ts +63 -12
  117. package/src/task-events.ts +232 -0
  118. package/src/team-samples.ts +74 -0
  119. package/src/team.ts +342 -0
  120. package/src/webclient.ts +45 -4
  121. package/src/webrequests.ts +5 -0
  122. package/src/websources.ts +30 -12
  123. package/src/workflow.ts +135 -18
  124. 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 either a **link** to a page of
32
- the application or a **submit request**:
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, call
934
- [`meta.send_mail`](#sending-e-mail) from the task definition's SQL or from an automation that
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 34 adds
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 (NL2IR) | 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* ([chapter 4](04-pages-and-regions.md#natural-language-filters-ask-in-your-own-words)) |
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) |