pgkiln 0.31.0 → 0.36.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 (131) hide show
  1. package/README.md +2 -2
  2. package/SECURITY.md +10 -0
  3. package/db/migrations/076_form_lost_update.sql +50 -0
  4. package/db/migrations/077_warn_unsaved.sql +41 -0
  5. package/db/migrations/078_url_region.sql +12 -0
  6. package/db/migrations/079_developer_toolbar.sql +53 -0
  7. package/db/migrations/080_search.sql +326 -0
  8. package/db/migrations/081_mail.sql +621 -0
  9. package/db/migrations/082_app_backups.sql +73 -0
  10. package/db/migrations/083_availability.sql +156 -0
  11. package/db/migrations/084_feedback.sql +46 -0
  12. package/db/migrations/085_page_behaviour.sql +65 -0
  13. package/db/migrations/086_data_settings.sql +599 -0
  14. package/db/migrations/087_login_page.sql +175 -0
  15. package/db/migrations/088_region_templates.sql +18 -0
  16. package/db/migrations/089_page_groups.sql +330 -0
  17. package/docs/apex-feature-parity.md +61 -39
  18. package/docs/guide/01-installation.md +1 -0
  19. package/docs/guide/03-builder.md +154 -4
  20. package/docs/guide/04-pages-and-regions.md +157 -3
  21. package/docs/guide/05-items.md +47 -0
  22. package/docs/guide/06-processing.md +210 -2
  23. package/docs/guide/07-dynamic-actions.md +60 -2
  24. package/docs/guide/08-security.md +56 -4
  25. package/docs/guide/09-reference.md +21 -0
  26. package/docs/guide/11-from-apex.md +9 -2
  27. package/docs/guide/12-development.md +40 -2
  28. package/docs/guide/14-globalization.md +15 -0
  29. package/docs/guide/15-extensions.md +1 -1
  30. package/docs/guide/17-mobile.md +3 -1
  31. package/docs/guide/18-cli.md +32 -1
  32. package/docs/guide/20-ai-agents.md +5 -2
  33. package/examples/hr/hr_50_search.sql +47 -0
  34. package/examples/hr/hr_51_mail.sql +70 -0
  35. package/examples/hr/hr_52_feedback.sql +15 -0
  36. package/examples/hr/hr_53_data_settings.sql +108 -0
  37. package/examples/hr/hr_54_region_templates.sql +67 -0
  38. package/examples/hr/hr_55_page_groups.sql +24 -0
  39. package/package.json +3 -1
  40. package/public/app.css +122 -3
  41. package/public/app.js +496 -13
  42. package/public/builder.css +22 -0
  43. package/public/builder.js +77 -0
  44. package/public/code-editor.css +3 -3
  45. package/public/sw.js +5 -3
  46. package/src/app.ts +10 -0
  47. package/src/appfiles.ts +13 -0
  48. package/src/automations.ts +12 -0
  49. package/src/builder/advisor.ts +68 -14
  50. package/src/builder/arrange.ts +2 -2
  51. package/src/builder/availability.ts +74 -0
  52. package/src/builder/backups.ts +134 -0
  53. package/src/builder/components.ts +123 -16
  54. package/src/builder/ddl-tools.ts +114 -0
  55. package/src/builder/designer.ts +35 -4
  56. package/src/builder/feedback.ts +82 -0
  57. package/src/builder/forms.ts +10 -5
  58. package/src/builder/home.ts +3 -0
  59. package/src/builder/locks.ts +15 -5
  60. package/src/builder/login-page.ts +82 -0
  61. package/src/builder/mail.ts +425 -0
  62. package/src/builder/newapp.ts +1 -1
  63. package/src/builder/region-help.ts +73 -0
  64. package/src/builder/region-settings.ts +19 -3
  65. package/src/builder/routes.ts +60 -5
  66. package/src/builder/search.ts +4 -4
  67. package/src/builder/shared.ts +3 -2
  68. package/src/builder/sql.ts +3 -0
  69. package/src/builder/ui.ts +9 -3
  70. package/src/cli/apps.ts +46 -5
  71. package/src/cli/main.ts +66 -5
  72. package/src/cli/mcp.ts +58 -4
  73. package/src/cli/replace.ts +40 -4
  74. package/src/ddl.ts +509 -0
  75. package/src/geocode.ts +44 -0
  76. package/src/i18n/ar.ts +56 -0
  77. package/src/i18n/cs.ts +56 -0
  78. package/src/i18n/da.ts +56 -0
  79. package/src/i18n/de.ts +56 -0
  80. package/src/i18n/el.ts +56 -0
  81. package/src/i18n/es.ts +56 -0
  82. package/src/i18n/fi.ts +56 -0
  83. package/src/i18n/fr.ts +56 -0
  84. package/src/i18n/he.ts +56 -0
  85. package/src/i18n/it.ts +56 -0
  86. package/src/i18n/ja.ts +56 -0
  87. package/src/i18n/ko.ts +56 -0
  88. package/src/i18n/nb.ts +56 -0
  89. package/src/i18n/pl.ts +56 -0
  90. package/src/i18n/pt.ts +56 -0
  91. package/src/i18n/ru.ts +56 -0
  92. package/src/i18n/sv.ts +56 -0
  93. package/src/i18n/tr.ts +56 -0
  94. package/src/i18n/uk.ts +56 -0
  95. package/src/i18n/zh.ts +56 -0
  96. package/src/i18n.ts +112 -0
  97. package/src/icons.ts +2 -1
  98. package/src/layout.ts +3 -2
  99. package/src/mail/mail.ts +148 -0
  100. package/src/mail/providers.ts +444 -0
  101. package/src/metadata.ts +34 -10
  102. package/src/runtime/ajax.ts +97 -0
  103. package/src/runtime/authz.ts +1 -1
  104. package/src/runtime/availability.ts +37 -0
  105. package/src/runtime/context.ts +46 -6
  106. package/src/runtime/data-export.ts +94 -0
  107. package/src/runtime/data-items.ts +194 -0
  108. package/src/runtime/devbar.ts +184 -0
  109. package/src/runtime/display-selector.ts +12 -4
  110. package/src/runtime/engine.ts +26 -1
  111. package/src/runtime/feedback.ts +128 -0
  112. package/src/runtime/grid.ts +170 -26
  113. package/src/runtime/items.ts +8 -0
  114. package/src/runtime/lists.ts +11 -1
  115. package/src/runtime/password-reset.ts +171 -0
  116. package/src/runtime/processes.ts +83 -2
  117. package/src/runtime/pwa.ts +7 -10
  118. package/src/runtime/regions.ts +41 -0
  119. package/src/runtime/render.ts +48 -7
  120. package/src/runtime/report-views.ts +4 -2
  121. package/src/runtime/report.ts +36 -22
  122. package/src/runtime/rest.ts +4 -0
  123. package/src/runtime/routes.ts +94 -37
  124. package/src/runtime/search.ts +252 -0
  125. package/src/runtime/signin.ts +80 -0
  126. package/src/runtime/url-region.ts +93 -0
  127. package/src/security.ts +11 -1
  128. package/src/server.ts +2 -0
  129. package/src/session.ts +8 -2
  130. package/src/static-version.ts +34 -0
  131. package/src/workingcopy.ts +2 -2
@@ -47,10 +47,52 @@ 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
- pgkiln doesn't send e-mail, so there is no self-service "forgot password" link (APEX apps don't
51
- have one out of the box either). A user who forgot their password asks an administrator, who
52
- sets a temporary one under **Users → account → Set password**, with *Require change of password
53
- on first use* ticked (the default).
50
+ A user who forgot their password asks an administrator, who sets a temporary one under
51
+ **Users → account → Set password**, with *Require change of password on first use* ticked (the
52
+ default), or uses **Forgot password?** when the application offers it (below).
53
+
54
+ ### Forgot password
55
+
56
+ With **Application settings → Sign-in methods → Offer "Forgot password?"** the sign-in page (the
57
+ built-in one or the application's [own login page](#the-login-page)) has a *Forgot password?* link
58
+ (APEX apps don't have one out of the box). It needs an enabled [mail service](06-processing.md#sending-e-mail)
59
+ with a From address for the application; without one the link isn't shown. It is for passwords
60
+ of the user directory only: not for LDAP, single sign-on, database accounts or custom
61
+ authentication.
62
+
63
+ 1. The user types their user name or e-mail address. The answer is always the same, whether or
64
+ not an account matched, so the page doesn't tell who has an account.
65
+ 2. An active account with a password and an e-mail address that may use the application gets an
66
+ e-mail (in the user's language) with a link to `/a/<alias>/password/reset?token=…`, built from
67
+ `PUBLIC_URL`, never from the request's `Host` header.
68
+ 3. The link works **once**, for **30 minutes**, in this application only. Opening it moves the
69
+ token into the session and out of the address bar; the user chooses a new password (the
70
+ [password policy](#passwords-and-my-account) applies).
71
+ 4. The new password ends every session and *Keep me signed in* token of the account, and makes
72
+ the account's other open links useless.
73
+
74
+ Only a SHA-256 hash of each token is stored (`meta.password_reset`, owner only). Requests are
75
+ limited to 5 per address in 15 minutes and 3 mails per account per hour; they count towards the
76
+ application's daily mail limit and show in the activity log (`password_reset_request`,
77
+ `password_reset`).
78
+
79
+ ### The login page
80
+
81
+ `/a/<alias>/login` shows pgkiln's sign-in page: the application's name, the sign-in methods and,
82
+ when the application has several languages, a language picker. To design your own (APEX: the
83
+ login page, usually page 9999), choose **Application settings → Sign-in methods → Create a login
84
+ page**: it adds page 9999 (or the next free number) that doesn't require authentication, with
85
+ page template *Minimal (no navigation)* and a **Login** region, and makes it the application's
86
+ **Login page**. From then on `/a/<alias>/login` (and every page that needs a signed-in user)
87
+ sends visitors there.
88
+
89
+ Edit it like any page: add static content, images, lists of links or any other region around the
90
+ Login region, which is the sign-in form (identity providers, user name and password, *Keep me
91
+ signed in*, *Forgot password?*). The form still posts to `/a/<alias>/login`, so throttling, the
92
+ CSRF check and every sign-in method work as on the built-in page; a failed sign-in comes back to
93
+ the login page with its message and the user name. A login page that requires authentication,
94
+ or doesn't exist, is ignored (the built-in page shows) and the Advisor reports it; so does a
95
+ login page without a Login region.
54
96
 
55
97
  With single sign-on, password changes happen at the identity provider; My account says so.
56
98
 
@@ -327,6 +369,16 @@ like for other users (an account of the same name, if there is one), or from the
327
369
  in authorization schemes. The login page has the user name and password form only: *Keep me signed
328
370
  in*, LDAP, single sign-on and the password-change form are not used by these apps.
329
371
 
372
+ ### Session timeout warning
373
+
374
+ Sessions end after the idle time (`SESSION_IDLE_MINUTES`, or Instance settings) and at their maximum
375
+ length (`SESSION_MAX_HOURS`). Signed-in users are warned before (Settings → *Errors and sessions* →
376
+ *Session timeout warning*, default 120 seconds; 0: no warning; APEX: Session Timeout Warning): a
377
+ dialog says when the session ends and offers **Stay signed in**, which marks the session active
378
+ (POST `/a/<alias>/session/extend`, with the session's CSRF token). At the maximum length the user can
379
+ only save their work; when the session has ended the dialog offers to sign in again and comes back
380
+ to the page.
381
+
330
382
  ### Keep me signed in
331
383
 
332
384
  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
@@ -47,12 +47,17 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
47
47
  | Rich Text Editor, Markdown Editor, Star Rating, Combobox, QR Code | Item types `richtext` (sanitised HTML), `markdown`, `rating`, `combobox` (colon-separated), `qrcode`; plus `daterange` (`from:to`) and `{"reveal": true}` on password items ([chapter 5](05-items.md)) |
48
48
  | Data Generator (sample data for development, 26.1) | SQL Workshop → **Sample Data**: generators proposed per column, foreign keys to existing parent rows, a seed, preview, insert in one transaction (parents first), SQL/CSV download, saved definitions ([chapter 16](16-files.md#sql-workshop--sample-data)) |
49
49
  | Data Workshop (load and unload: CSV, Excel, JSON, XML) / Data Load Definition, *Execute Data Load* process | SQL Workshop → Load Data and **Unload Data** (CSV, JSON, Excel, XML; [chapter 16](16-files.md#sql-workshop--unload-data)); Shared Components → Data load definitions; process type `data_load` with `"definition"` ([chapter 16](16-files.md#data-loading)) |
50
+ | SQL Workshop → Utilities → Generate DDL, Schema Comparison | SQL Workshop → **DDL** and **Compare**: a schema's DDL in an order that runs; two schemas compared by their DDL with a change script; also `pgkiln ddl` and `pgkiln compare` ([chapter 3](03-builder.md#sql-workshop)) |
50
51
  | SQL Workshop → SQL Scripts, Quick SQL, Query Builder | The same names in the SQL Workshop: saved scripts with a result per statement and a run history, shorthand → PostgreSQL DDL, a SELECT built on a canvas of tables, joined by foreign keys or columns you connect, with column functions and grouping ([chapter 3](03-builder.md#sql-workshop)) |
51
52
  | Interactive report *Download → PDF*, printing | Actions → Download PDF, Print ([chapter 16](16-files.md#printing)) |
52
53
  | 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
54
  | `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
55
  | 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 | Not included: pgkiln doesn't send mail. Queue mail in a table and deliver it with your own service, or use an extension such as `pg_smtp_client` ([chapter 15](15-extensions.md)) |
56
+ | `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 |
57
+ | `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)) |
58
+ | `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)) |
59
+ | `APEX_LANG.LANG`, dynamic translations | Shared Components → Dynamic translations and `meta.lang(text)` ([chapter 14](14-globalization.md#dynamic-translations)) |
60
+ | Display Image, Geocoded Address items | Item types `image` and `address` (geocoder: `GEOCODER_URL`) ([chapter 5](05-items.md#display-image-and-geocoded-address)) |
56
61
  | 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
62
  | Application date format mask | Settings → Globalization → Date format (Oracle-style masks) |
58
63
  | 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)) |
@@ -93,7 +98,9 @@ in to a second app is then silent via the identity provider's session. See
93
98
  Active Directory** passwords work the same way (groups → roles), and *Keep me signed in* matches
94
99
  APEX's persistent authentication. **Database accounts** sign in with PostgreSQL login roles, and
95
100
  **custom authentication** calls your own PL/pgSQL function (APEX: a custom PL/SQL scheme), see
96
- [chapter 8](08-security.md#custom-authentication-a-plpgsql-function).
101
+ [chapter 8](08-security.md#custom-authentication-a-plpgsql-function). The **login page** (APEX: page 9999 with
102
+ the Login region template) is a page you design in the Page Designer around a **Login** region, and
103
+ **Forgot password?** is built in (APEX apps build their own), see [chapter 8](08-security.md#the-login-page).
97
104
 
98
105
  ## ORDS and PostgREST
99
106
 
@@ -34,6 +34,8 @@ src/
34
34
  app.ts / server.ts Fastify setup / entry point
35
35
  db.ts the two pools, appTx() (SET LOCAL ROLE + pgkiln.* settings, NOTICEs to the debug log), savepoints
36
36
  security.ts URL checksums, password policy, security headers (CSP nonce), throttling limits
37
+ ddl.ts Generate DDL and Schema Comparison: readSchema (catalog → one DDL per object, unqualified), ddlScript (order), compare, changeScript
38
+ static-version.ts the version of app.css, app.js, icons.svg and sw.js (?v= in their URLs, the service worker version): content hash, looked at again when a file changes
37
39
  session.ts sessions (hashed tokens), activity log, login throttling
38
40
  sso.ts OpenID Connect: discovery, sign-in flow, ID token checks, account linking
39
41
  saml.ts SAML 2.0 sign-in (node-saml): AuthnRequest, response checks, SP metadata
@@ -68,6 +70,7 @@ src/
68
70
  css.ts PageCss: data-dependent styles as classes in the page's nonce'd <style> (CSP)
69
71
  metadata.ts types + loaders for apps and pages (components of excluded build options are left out here)
70
72
  maptiles.ts map tile server URL, attribution and CSP origin
73
+ geocode.ts the installation's geocoder for address items (GEOCODER_URL, Nominatim search API)
71
74
  mvt.ts Mapbox Vector Tile encoding for map layers served as tiles
72
75
  yamltext.ts the text style of the directory export: a strict YAML subset, written and read
73
76
  webclient.ts outgoing HTTP to web services: allow-list, address checks at connect time (SSRF), redirects, limits
@@ -80,6 +83,11 @@ src/
80
83
  webrequests.ts web requests from SQL (meta.web_request, migration 052): runPending() after each sql page process
81
84
  (same transaction), webRequestTick() for committed ones (automations scheduler), retention purge;
82
85
  calls go through websources.ts call()/invoke() (allow-list, SSRF checks, credentials)
86
+ mail/ e-mail (migration 081): providers.ts (one deliver() per provider: SMTP through nodemailer with a password
87
+ or XOAUTH2, Microsoft Graph sendMail, Amazon SES v2 raw MIME with signV4, Resend, Postmark, SendGrid,
88
+ Mailgun, log; MailError says whether a retry may help; client-credentials token cache; settingsProblems),
89
+ mail.ts (sendWith(): service enabled, allowed sender, secret decrypted; mailTick(): claim with SKIP LOCKED,
90
+ send, retry after 1/4/16/64 minutes, purge after 30 days; the NOTIFY pgkiln_mail listener)
83
91
  ai/ AI services (migration 060): types.ts (the provider interface, AiError kinds),
84
92
  anthropic.ts (Claude through @anthropic-ai/sdk: streamed, effort, structured outputs via
85
93
  output_config.format, refusal fallbacks, stop_reason checks, typed SDK errors), openai.ts (the openai
@@ -93,6 +101,8 @@ src/
93
101
  icons.ts icon helper (sprite in public/icons.svg)
94
102
  runtime/
95
103
  routes.ts HTTP handlers: show, submit, dynamic actions, cascading lists, login
104
+ signin.ts the sign-in form (built-in page and the login region of an application's own login page), safeNext, passwordResetAvailable
105
+ password-reset.ts Forgot password: request (same answer either way), the link (token into the session), the new password
96
106
  context.ts PageContext, dbg()/timed() debug helpers, bind values, substitutions, public error messages, writeOut (streamed responses with back pressure)
97
107
  authz.ts authorization schemes, conditions, visibility (menu requests count as buttons)
98
108
  engine.ts form fetch, validations, processes (conditions, execution chains, queueing background chains; web requests queued by an sql process are made right after it), application processes
@@ -103,7 +113,7 @@ src/
103
113
  style (user choice, default), themeCss() (only hex values and constants reach the CSS)
104
114
  template-options.ts template options: the fixed CSS class list per region and button, templateClasses() (unknown values ignored)
105
115
  regions.ts region shell + chart (drill-down links, gauge settings)/cards/dynamic dispatch with row limits, lazy placeholder and cache, buttons (menu buttons, badges)
106
- report.ts, report-views.ts (group by, pivot, chart), compute.ts (computed column expressions), grid.ts (aggregates, row actions, Actions menu, layoutFromForm), grid-layout.ts (column layouts: clean, arrange, per user), master-detail.ts (signed master row selection, details), facets.ts, items.ts
116
+ report.ts, report-views.ts (group by, pivot, chart), compute.ts (computed column expressions), grid.ts (aggregates, row actions, Actions menu, single row view, layoutFromForm), grid-layout.ts (column layouts: clean, arrange, per user), master-detail.ts (signed master row selection, details), facets.ts, items.ts
107
117
  (report.ts: paging with row ranges and max_rows, keyset paging (keysetPlan, seekCondition, signed r<id>_k), pagerNav, streamed CSV/Excel downloads with a cursor;
108
118
  items.ts: lovOptions, searchLov/lovLookup for popup LOVs, served by POST /a/:alias/:page/lov/:item/search in routes.ts)
109
119
  region-cache.ts region caching (keys per scope, CSRF placeholder, invalidation on submit) and lazy regions (GET …/region/:id is in routes.ts)
@@ -142,6 +152,21 @@ src/
142
152
  session, formatAnswer (escaped), send/clear routes
143
153
  ai-filter.ts natural-language filters on a report ("ai_filter"): columns → structured output → checked → r<id>_f/q/s/d
144
154
  pdf.ts report PDFs with report layouts (pdfkit); rows from a cursor in batches (tablePdf takes batches)
155
+ search.ts search region and search configurations (migration 080): words as ILIKE parameters, table/sql/list
156
+ sources as the app role in savepoints, marked results, configColumns/searchConfigProblems for the Advisor
157
+ url-region.ts URL region (migration 078): literal origins only (urlTarget), &ITEM. URL-encoded, sandbox and height,
158
+ frameOrigins() for the CSP's frame-src (security.ts)
159
+ ajax.ts Ajax callbacks (migration 085: POST …/ajax/:name, point "ajax" processes, x01 … x10 binds) and the
160
+ download dynamic action (POST …/da/:id/download)
161
+ availability.ts availabilityRefusal(): who may use an application (migration 083); loadContext, the login page
162
+ and REST modules ask it (PostgREST: meta.api_check)
163
+ feedback.ts users' feedback page and the navigation bar entry (migration 084)
164
+ data-items.ts display image and address items (migration 086): imageUrl (https origins into the CSP's img-src),
165
+ GET …/image/:item (a BLOB from the item's query), POST …/geocode/:item, and items kept per user
166
+ (loadPreferences / savePreferences through meta.get_preference / set_preference)
167
+ data-export.ts the download process's {"export": …}: meta.data_export for csv/json/xml/html, Excel and PDF here
168
+ devbar.ts developer toolbar and session state viewer (migration 079): the pgkiln_devbar token of a builder
169
+ session (ensureDevbar/devbarOf), the toolbar markup, the viewer, clear and per-session debug routes
145
170
  builder/
146
171
  components.ts property spec of every component (drives the property editor)
147
172
  ui.ts IDE shell (icon rail, toolbar, breadcrumb, status bar), builder theme, form helpers, CSRF check, app tabs
@@ -158,6 +183,14 @@ src/
158
183
  appwizard.ts Create → From pasted data (kept as a temp file, then the From a file steps) and From existing tables
159
184
  (a schema's tables/views → report+form or report pages, navigation, dashboard)
160
185
  forms.ts generic component property form (lookups, render, save)
186
+ backups.ts Application → Backups (migration 082): list, back up now, download, restore (replaceApp), keep N;
187
+ backupTick() for the scheduler's daily backups of changed applications
188
+ availability.ts Application → Availability (migration 083): status, message, users, build status (Run only is
189
+ enforced by locks.ts blockingLock); availabilityBadge() in the application header
190
+ feedback.ts Application → Feedback (migration 084): the setting, the list with filters, status and answers
191
+ region-help.ts the Attributes (JSON) help per region type, with links to the guide
192
+ mail.ts Workspace utilities → Mail services (administrators: providers, write-only secret, allowed senders,
193
+ applications with From and daily limit, test) and Applications → Activity → Mail (retry, cancel)
161
194
  shared.ts Shared Components and access control
162
195
  designer.ts page designer: component tree (with computations and branches), layout canvas and gallery, property editor, toolbar
163
196
  arrange.ts page designer layout changes: move, column span, create from the gallery, undo / redo
@@ -177,6 +210,8 @@ src/
177
210
  region-settings.ts page designer: settings forms for grid, chart (gauge, drill-down), cards, calendar (views, create, drag and drop), facets, smart filters, display selector, list
178
211
  search.ts app search, "where used" (appEntries, search, whereUsed, usedInPanel)
179
212
  advisor.ts Advisor: EXPLAIN every SQL fragment, reference checks, plpgsql_check
213
+ ddl-tools.ts SQL Workshop → DDL and Compare (src/ddl.ts)
214
+ login-page.ts Application settings → Login page and Forgot password; Create a login page (page 9999)
180
215
  top-sql.ts Top SQL per app role from pg_stat_statements
181
216
  diagnostics.ts Activity → Debug messages (level, list, one request's entries, purge); Workspace utilities →
182
217
  Installation (version, install/upgrade runs, applied and missing migrations; administrators)
@@ -377,5 +412,8 @@ browser enhancements at the end of `public/app.js` (the item must work without t
377
412
  1. Update `CHANGELOG.md` and the version in `package.json`.
378
413
  2. Merge to `main`; CI must be green (every job). The `main` branch should be protected on
379
414
  GitHub (Settings → Branches → rule for `main`: require the CI checks).
380
- 3. Tag: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
415
+ 3. Tag: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`. The tag publishes the Docker image
416
+ (`docker-publish.yml`: `pgkiln/pgkiln` and `ghcr.io/pgkiln/pgkiln`, amd64 and arm64) and the npm package
417
+ (`npm-publish.yml`, trusted publishing with provenance; it refuses a tag that doesn't match
418
+ `package.json`). Then `gh release create vX.Y.Z` with the changelog section as notes.
381
419
  4. Add the new tag to the `upgrade` job's matrix in `.github/workflows/ci.yml` (see above).
@@ -98,6 +98,21 @@ raise exception '%', meta.message('LEAVE_OVERLAP', to_char(p_start, 'DD-MM-YYYY'
98
98
 
99
99
  The HR sample's dashboard uses them for its key figures (`examples/hr/hr_04_i18n.sql`).
100
100
 
101
+ ### Dynamic translations
102
+
103
+ For **values from tables** (department names, statuses, list of values entries), use dynamic
104
+ translations (APEX: *Dynamic Translations*, `APEX_LANG.LANG`): **Shared Components → Dynamic
105
+ translations** holds a language, the text as stored and its translation, and `meta.lang` returns
106
+ the text in the session's language:
107
+
108
+ ```sql
109
+ select meta.lang(dname) as d, deptno as r from hr.dept order by 1 -- a list of values
110
+ ```
111
+
112
+ An exact language match wins, then the base language (`nl-BE` → `nl`), else the text as it is.
113
+ `meta.lang(text, 'de')` asks for another language. Dynamic translations are exported with the
114
+ application (`globalization/dynamic-translations.yaml` in an application's files).
115
+
101
116
  ### Date formats
102
117
 
103
118
  Dates and timestamps in reports, grids and display items are formatted with a **mask**:
@@ -109,7 +109,7 @@ Audit triggers record such changes as made by `nobody`, which is what you want f
109
109
  | **[pgmq](https://github.com/pgmq/pgmq)** | A message queue in Postgres for background work handled by a worker service | Like AWS SQS, transactional |
110
110
  | **pg_partman** | Partition big append-only tables such as `meta.activity_log` by month | Only needed at high volume |
111
111
  | **[plv8](https://github.com/plv8/plv8)** | Server-side JavaScript functions | Like APEX's MLE JavaScript. Maintained upstream, but some hosts (e.g. Supabase on PostgreSQL 17) no longer offer it, so check before depending on it |
112
- | **[pg_smtp_client](https://github.com/brianpursley/pg_smtp_client)** | Sending e-mail from SQL, if your team needs it | pgkiln deliberately doesn't send mail ([chapter 11](11-from-apex.md)). This keeps mail out of pgkiln and in your own schema |
112
+ | **[pg_smtp_client](https://github.com/brianpursley/pg_smtp_client)** | Sending e-mail synchronously from SQL, outside an application | pgkiln itself sends mail queued with `meta.send_mail` after the commit, through SMTP, Microsoft Graph, Amazon SES or an HTTP API ([chapter 6](06-processing.md#sending-e-mail)); this extension talks SMTP from the database itself |
113
113
 
114
114
  ## Security notes
115
115
 
@@ -27,7 +27,9 @@ Everything is per application, under its own address: `/a/<alias>/manifest.webma
27
27
  ## Offline
28
28
 
29
29
  - **The app shell** (pgkiln's CSS, script and icons) and an offline page are stored when the app is
30
- installed, so the app always opens.
30
+ installed, so the app always opens. Pages ask for these files with a version in the URL
31
+ (`/static/app.css?v=…`, from the files' contents); when pgkiln is upgraded, or a file changes,
32
+ devices fetch the new one at the next page load.
31
33
  - **Pages** are fetched from the network first. Without a connection, a page the user opened before
32
34
  comes from the device (when *Keep visited pages* is on); otherwise the offline page appears,
33
35
  listing the pages that are on the device.
@@ -35,8 +35,10 @@ any (see [what is not exported](03-builder.md#export-format)).
35
35
  | `pgkiln migrate [--example <name>]` | Applies the migrations that were not applied yet, then optionally `examples/<name>/` (same as `npm run db:migrate` / `npm run example:hr`; `--root` and `--seed` as in `scripts/migrate.ts`) |
36
36
  | `pgkiln apps [--json]` | Lists the applications: alias, number of pages, name |
37
37
  | `pgkiln export <alias> [--format json\|dir\|text] [--out <path>]` | `json` (default): the `pgkiln/2` document with sorted keys, to `--out` or standard output. `dir`: a directory (default `./<alias>`), see below. `text`: the same directory in YAML with the code inline ([text files](#text-files-apexlang)) |
38
- | `pgkiln import <path> [--alias <alias>] [--replace]` | Imports a JSON export, an application directory or a `.zip` of one. `--alias` gives the copy another alias. `--replace` updates the application with that alias in place |
38
+ | `pgkiln import <path> [--alias <alias>] [--replace] [--check [--force] \| --dry-run]` | Imports a JSON export, an application directory or a `.zip` of one. `--alias` gives the copy another alias. `--replace` updates the application with that alias in place (after a [backup](03-builder.md#backups)). `--check` checks the application before it commits, `--dry-run` only checks ([below](#checking-before-an-import)) |
39
39
  | `pgkiln diff <alias> <path> [--name-only \| --quiet]` | What differs between the application in the database and a directory (or JSON file, or zip) |
40
+ | `pgkiln ddl <schema> [--no-comments] [--no-grants] [--out <file>]` | The DDL of a schema's objects, in an order that runs (as SQL Workshop → DDL, [chapter 3](03-builder.md#sql-workshop)) |
41
+ | `pgkiln compare <source> <target> [--script]` | Compares two schemas: `+` only in the source, `-` only in the target, `~` different; exit code 1 when they differ. `--script` writes the script that makes the target like the source (drops commented out); nothing is run |
40
42
  | `pgkiln mcp` | An MCP server on standard input/output for AI coding agents, see [chapter 20](20-ai-agents.md) |
41
43
  | `pgkiln plugin build <dir> [-o <file>]` | Builds a plug-in file (`pgkiln-plugin/2`) from a source directory, see [plug-ins](04-pages-and-regions.md#plug-ins-with-their-own-code) |
42
44
  | `pgkiln plugin install <file\|dir> --app <alias> [--replace]` | Adds a plug-in (file or source directory) to an application; its install SQL is not run |
@@ -187,6 +189,9 @@ the files any more are removed. What belongs to this installation stays:
187
189
  [chapter 19](19-rest-data-sources.md#web-credentials)).
188
190
  - the **style variant each user chose** ([chapter 14](14-globalization.md#style-variants-theme-roller));
189
191
  a style that the file no longer has falls back to the default.
192
+ - its **availability and build status** ([chapter 3](03-builder.md#availability-and-build-status)): exports
193
+ leave them out, so a deployment doesn't open an application you closed, or close one;
194
+ - its **backups**: the version the import replaces is the newest one ([chapter 3](03-builder.md#backups)).
190
195
 
191
196
  It all happens in one transaction; any error leaves the application unchanged. Without an
192
197
  application with that alias, `--replace` simply imports, so the same command deploys the first
@@ -201,6 +206,32 @@ pgkiln diff hr apps/hr # on the target: what would cha
201
206
  pgkiln import apps/hr --replace # deploy
202
207
  ```
203
208
 
209
+ ### Checking before an import
210
+
211
+ An import doesn't need the application's tables, columns and functions to exist (neither does APEX's):
212
+ a page whose SQL names a missing table imports fine and fails when it runs. `--check` asks the
213
+ [Advisor](03-builder.md#advisor) first, inside the import's transaction, before anything is committed:
214
+ every SQL fragment is planned as the application's database role (unknown tables, columns,
215
+ functions and types, missing grants), PL/pgSQL blocks are compiled, and references to pages,
216
+ items, lists of values, schemes and search configurations are checked.
217
+
218
+ ```sh
219
+ pgkiln import apps/hr --replace --check # errors: nothing changes, exit code 1
220
+ pgkiln import apps/hr --replace --dry-run # only check (exit code 0 without errors)
221
+ pgkiln import apps/hr --replace --check --force # report, import anyway
222
+ ```
223
+
224
+ ```text
225
+ ERROR page 2, region Employees (Source): relation "hr.employee" does not exist
226
+ WARNING page 3, item P3_SAL (Default value): …
227
+ Check: 1 error(s), 1 warning(s), 0 note(s).
228
+ Not imported: the check found 1 error(s). Fix them, or use --force.
229
+ ```
230
+
231
+ Deploy the database changes (your migration scripts) before the application, so the check sees
232
+ the tables the new version needs. The MCP server's `import_app` checks by default; the builder's
233
+ Import page has the check ticked.
234
+
204
235
  `export --format dir` into an existing directory removes the files of deleted components and
205
236
  leaves dot files (`.git`, `.gitattributes`) alone; it refuses a non-empty directory without a
206
237
  `pgkiln.json`, so a typo can't empty the wrong folder.
@@ -48,12 +48,14 @@ directory the agent started the server in (the project).
48
48
  | `get_page` | every component of one page as YAML, SQL inline | no |
49
49
  | `read_app_files` | component files by path or prefix (`shared/lovs/`, `navigation.yaml`, …) | no |
50
50
  | `describe_schema` | schemas with tables, views and functions; or one table with columns, constraints, indexes, RLS policies and triggers | no |
51
+ | `generate_ddl` | the DDL of a schema's objects, in an order that runs: a start for a migration script | no |
52
+ | `compare_schemas` | the differences between two schemas and a script that makes the target like the source (not run) | no |
51
53
  | `run_query` | one SQL statement, read-only, rows as JSON | no |
52
54
  | `search_docs` | searches this user guide, the parity matrix and SECURITY.md | no |
53
55
  | `recent_errors` | the latest errors and refused requests from the activity log | no |
54
56
  | `export_app` | writes an application to a directory (default `apps/<alias>`, format `text`) | files on disk |
55
57
  | `diff_app` | what an import of a directory would change (A/D/M with unified diffs); also reports files it can't read | no |
56
- | `import_app` | imports a directory, zip or JSON file; `replace: true` updates the application in place | the database |
58
+ | `import_app` | imports a directory, zip or JSON file; `replace: true` updates the application in place. It checks the application first (the Advisor, in the import's transaction): errors refuse the import and are listed; `force: true` imports anyway, `dry_run: true` only checks | the database |
57
59
 
58
60
  The files are the [directory format](18-cli.md#the-directory-format) of the command line, in the
59
61
  [text style](18-cli.md#text-files-apexlang): one YAML file per component, with SQL, PL/pgSQL and
@@ -67,7 +69,8 @@ users, sessions and saved reports kept, supporting objects and plug-in install S
67
69
  `describe_schema` for the tables; `search_docs` before guessing a property or region type.
68
70
  2. `export_app` to `apps/<alias>`, then edit the YAML files there.
69
71
  3. `diff_app`, so you (and it) see exactly what will change.
70
- 4. `import_app` with `replace: true`. Open the page in the browser to check.
72
+ 4. `import_app` with `replace: true`. When it names errors (a missing column, a typo in SQL), the agent fixes
73
+ the files and imports again; nothing was changed. Open the page in the browser to check.
71
74
 
72
75
  Tables, views and functions are ordinary PostgreSQL objects: an agent changes them in your
73
76
  migration scripts, not in the application files (`run_query` can't change them anyway).
@@ -0,0 +1,47 @@
1
+ -- =====================================================================
2
+ -- HR example, part 50: search configurations and the search region
3
+ -- (migration 080; docs/guide/04-pages-and-regions.md, search region)
4
+ --
5
+ -- Page 42 "Search": one field searches three sources as you type:
6
+ -- - EMPLOYEES: a query (employees with their department), each result
7
+ -- opens the employee's form (page 3);
8
+ -- - DEPARTMENTS: the table hr.dept, each result opens page 5;
9
+ -- - SHORTCUTS: the entries of the list HR_SHORTCUTS the user may see.
10
+ -- =====================================================================
11
+
12
+ insert into meta.search_config (app_id, name, label, seq, source_type, source, searchable_columns,
13
+ title_column, subtitle_column, badge_column, icon, link)
14
+ select a.id, c.name, c.label, c.seq, c.source_type, c.source, c.searchable, c.title, c.subtitle, c.badge, c.icon, c.link::jsonb
15
+ from meta.app a, (values
16
+ ('EMPLOYEES', 'Employees', 10, 'sql',
17
+ $q$select e.empno, initcap(e.ename) as name, initcap(e.job) as job, initcap(d.dname) as department
18
+ from hr.emp e left join hr.dept d on d.deptno = e.deptno$q$,
19
+ '{name,job,department}'::text[], 'name', 'job', 'department', 'user', '{"page": 3, "items": {"P3_EMPNO": "#EMPNO#"}}'),
20
+ ('DEPARTMENTS', 'Departments', 20, 'table', 'hr.dept', '{dname,loc}'::text[], 'dname', 'loc', null, 'building',
21
+ '{"page": 5, "items": {"P5_DEPTNO": "#DEPTNO#"}}'),
22
+ ('SHORTCUTS', 'Shortcuts', 30, 'list', 'HR_SHORTCUTS', '{}'::text[], null, null, null, 'link', null)
23
+ ) as c (name, label, seq, source_type, source, searchable, title, subtitle, badge, icon, link)
24
+ where a.alias = 'hr';
25
+
26
+ insert into meta.page (app_id, page_no, name, title, parent_page)
27
+ select id, 42, 'Search', 'Search', 1 from meta.app where alias = 'hr';
28
+
29
+ insert into meta.region (page_id, seq, title, type, columns, template, config)
30
+ select p.id, 10, 'Search', 'search', 12, 'standard',
31
+ '{"configs": ["EMPLOYEES", "DEPARTMENTS", "SHORTCUTS"], "placeholder": "Search employees, departments and shortcuts…"}'
32
+ from meta.page p join meta.app a on a.id = p.app_id where a.alias = 'hr' and p.page_no = 42;
33
+
34
+ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
35
+ select id, 2, 'Search', 'search', 42 from meta.app where alias = 'hr';
36
+
37
+ insert into meta.translation (app_id, language, source, target)
38
+ select a.id, 'nl', t.source, t.target
39
+ from meta.app a, (values
40
+ ('Search', 'Zoeken'),
41
+ ('Employees', 'Medewerkers'),
42
+ ('Departments', 'Afdelingen'),
43
+ ('Shortcuts', 'Snelkoppelingen'),
44
+ ('Search employees, departments and shortcuts…', 'Zoek medewerkers, afdelingen en snelkoppelingen…')
45
+ ) as t (source, target)
46
+ where a.alias = 'hr'
47
+ on conflict do nothing;
@@ -0,0 +1,70 @@
1
+ -- =====================================================================
2
+ -- HR example, part 51: e-mail (migration 081; docs/guide/06-processing.md,
3
+ -- "Sending e-mail")
4
+ --
5
+ -- The example sends through a mail service of provider "log": the server
6
+ -- writes each mail to its log and sends nothing, so the example works on
7
+ -- any installation. An administrator points HR at a real service under
8
+ -- Workspace utilities → Mail services (SMTP, Microsoft 365, Amazon SES,
9
+ -- Resend, Postmark, SendGrid or Mailgun).
10
+ --
11
+ -- The template LEAVE_DECIDED tells an employee that a leave request was
12
+ -- approved or rejected; a trigger queues it from the running application
13
+ -- when the status changes (the decision's transaction: a rollback sends
14
+ -- nothing). A mail that can't be queued never stops the decision.
15
+ -- =====================================================================
16
+
17
+ insert into meta.mail_service (name, description, provider, senders, settings)
18
+ values ('DEVELOPMENT_LOG', 'Writes mail to the server log; sends nothing', 'log', '{@example.com}', '{"default_from": "hr@example.com"}')
19
+ on conflict (name) do nothing;
20
+
21
+ insert into meta.app_mail_service (app_id, service_id, from_address, max_per_day)
22
+ select a.id, s.id, 'hr@example.com', 500
23
+ from meta.app a, meta.mail_service s
24
+ where a.alias = 'hr' and s.name = 'DEVELOPMENT_LOG'
25
+ on conflict (app_id) do nothing;
26
+
27
+ insert into meta.email_template (app_id, name, subject, html_body, text_body)
28
+ select id, 'LEAVE_DECIDED', 'Your leave request was #STATUS#',
29
+ $h$<p>Dear #NAME#,</p>
30
+ <p>Your leave request from <b>#START#</b> to <b>#END#</b> was <b>#STATUS#</b> by #DECIDED_BY#.</p>
31
+ <p>Kind regards,<br>HR</p>$h$,
32
+ $t$Dear #NAME#,
33
+
34
+ Your leave request from #START# to #END# was #STATUS# by #DECIDED_BY#.
35
+
36
+ Kind regards,
37
+ HR$t$
38
+ from meta.app where alias = 'hr';
39
+
40
+ create function hr.mail_leave_decided() returns trigger
41
+ language plpgsql security definer set search_path = hr, pg_catalog as $$
42
+ declare
43
+ v_email text;
44
+ v_name text;
45
+ begin
46
+ if new.status not in ('APPROVED', 'REJECTED') or meta.app_id() is null then
47
+ return new;
48
+ end if;
49
+ select a.email, initcap(e.ename) into v_email, v_name
50
+ from hr.emp e join meta.account a on lower(a.username) = lower(e.username)
51
+ where e.empno = new.empno;
52
+ if v_email is null then
53
+ return new;
54
+ end if;
55
+ perform meta.send_mail_template('LEAVE_DECIDED', jsonb_build_object(
56
+ 'NAME', v_name,
57
+ 'STATUS', lower(new.status),
58
+ 'START', to_char(new.start_date, 'DD Mon YYYY'),
59
+ 'END', to_char(new.end_date, 'DD Mon YYYY'),
60
+ 'DECIDED_BY', coalesce(new.decided_by, 'HR')), v_email);
61
+ return new;
62
+ exception when others then
63
+ raise warning 'hr.mail_leave_decided: %', sqlerrm;
64
+ return new;
65
+ end
66
+ $$;
67
+
68
+ create trigger leave_decided_mail after update of status on hr.leave_request
69
+ for each row when (old.status is distinct from new.status)
70
+ execute function hr.mail_leave_decided();
@@ -0,0 +1,15 @@
1
+ -- =====================================================================
2
+ -- HR example, part 52: feedback (migration 084; docs/guide/03-builder.md,
3
+ -- "Feedback")
4
+ --
5
+ -- Users send feedback about a page from the navigation bar's Feedback link;
6
+ -- developers answer under Application → Activity → Feedback. One answered
7
+ -- example from the user king.
8
+ -- =====================================================================
9
+
10
+ update meta.app set feedback = true where alias = 'hr';
11
+
12
+ insert into meta.feedback (app_id, page_no, username, kind, rating, body, status, response, responded_by, responded_at)
13
+ select id, 2, 'king', 'idea', 4, 'Could the employee list show the hire date as well?', 'done',
14
+ 'Added in the column list: open Actions → Columns and show "Hiredate".', 'admin', now()
15
+ from meta.app where alias = 'hr';