pgkiln 0.52.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 (53) hide show
  1. package/README.md +1 -1
  2. package/SECURITY.md +5 -1
  3. package/db/migrations/125_app_change.sql +23 -0
  4. package/db/migrations/126_mail_queue_network_access.sql +119 -0
  5. package/docs/apex-feature-parity.md +40 -11
  6. package/docs/guide/01-installation.md +1 -1
  7. package/docs/guide/03-builder.md +76 -3
  8. package/docs/guide/06-processing.md +6 -1
  9. package/docs/guide/08-security.md +55 -0
  10. package/docs/guide/09-reference.md +3 -1
  11. package/docs/guide/11-from-apex.md +1 -0
  12. package/docs/guide/12-development.md +43 -8
  13. package/docs/guide/14-globalization.md +49 -1
  14. package/docs/guide/18-cli.md +21 -1
  15. package/docs/guide/19-rest-data-sources.md +12 -0
  16. package/docs/guide/20-ai-agents.md +8 -2
  17. package/examples/hr/hr_70_data_security.sql +79 -0
  18. package/package.json +1 -1
  19. package/public/app.css +97 -23
  20. package/public/builder.css +50 -0
  21. package/public/builder.js +4 -0
  22. package/src/app.ts +9 -0
  23. package/src/builder/app-updates.ts +55 -0
  24. package/src/builder/data-security.ts +182 -0
  25. package/src/builder/home.ts +16 -5
  26. package/src/builder/mail-queue.ts +212 -0
  27. package/src/builder/mail.ts +72 -45
  28. package/src/builder/network-access.ts +140 -0
  29. package/src/builder/object-actions.ts +1 -0
  30. package/src/builder/object-browser.ts +1 -1
  31. package/src/builder/rest-catalogs.ts +19 -4
  32. package/src/builder/sql.ts +2 -0
  33. package/src/builder/themeroller.ts +47 -6
  34. package/src/builder/ui.ts +5 -0
  35. package/src/builder/websources.ts +1 -1
  36. package/src/builder/workspace-mail.ts +121 -0
  37. package/src/builder/workspaces.ts +1 -0
  38. package/src/cli/main.ts +25 -3
  39. package/src/cli/mcp.ts +40 -4
  40. package/src/cli/only.ts +52 -0
  41. package/src/cli/replace.ts +5 -1
  42. package/src/datasec.ts +343 -0
  43. package/src/mail/mail.ts +50 -11
  44. package/src/mail/providers.ts +6 -3
  45. package/src/metadata.ts +2 -0
  46. package/src/network-acl.ts +126 -0
  47. package/src/objectstore.ts +3 -0
  48. package/src/runtime/engine.ts +3 -1
  49. package/src/runtime/render.ts +4 -2
  50. package/src/runtime/styles.ts +54 -12
  51. package/src/webclient.ts +45 -4
  52. package/src/webrequests.ts +5 -0
  53. package/src/websources.ts +30 -12
@@ -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
@@ -65,6 +65,7 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
65
65
  | Automatic Time Zone | Settings → Globalization → Time zone and Automatic time zone; My account → Time zone ([chapter 14](14-globalization.md#time-zones)) |
66
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)) |
67
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)) |
68
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)) |
69
70
  | ORDS | Not needed to serve apps; REST APIs with PostgREST, see [below](#ords-and-postgrest) |
70
71
  | Export `f123.sql` / APEXlang | `meta.export_app('alias')` (JSON) |
@@ -42,7 +42,8 @@ src/
42
42
  merge into the main application or refresh the copy, both through cli/replace.ts
43
43
  cli/ the command line: main.ts (commands, help, exit codes), apps.ts (connect, export,
44
44
  read, import), files.ts (directories, zip), diff.ts, replace.ts (import --replace in place),
45
- mcp.ts (`pgkiln mcp`: the MCP server for AI coding agents, chapter 20)
45
+ mcp.ts (`pgkiln mcp`: the MCP server for AI coding agents, chapter 20), only.ts (sprint 116:
46
+ `import --replace --only pages/12`, the named files merged over the application's current export)
46
47
  app.ts / server.ts Fastify setup / entry point
47
48
  db.ts the owner and shared runtime pools, `runtime` (the current scope's: runtimeScope), appTx() (SET LOCAL ROLE + pgkiln.* settings, NOTICEs to the debug log), savepoints
48
49
  workspace-db.ts isolated workspaces (090): the isolated applications (cached 5 s), a pool per workspace login (passwords from
@@ -95,6 +96,9 @@ src/
95
96
  objects, type reporting, roles viewer/editor/administrator as schemes EDITOR and ADMIN, page 1 a Data
96
97
  Reporter), reportingApps(), reportingAppCount() (meta.reporting_app_count, for the workspace home)
97
98
  tabledef.ts (sprint 95) a table as a definition: readTable(), checkTable(), tableSql() (CREATE, or ALTER from the catalog's version)
99
+ datasec.ts (sprint 119, WS-13) data security per table: readSecurity() (policies, column privileges per role, the
100
+ masking view and its configuration in the view comment), checkSecurity(), securitySql() (GRANT / REVOKE,
101
+ the masking view owned by <schema>_masking); the page is builder/data-security.ts
98
102
  sqlformat.ts (sprint 93) formatSql(): the worksheet's Format (clauses per line, keyword case; strings and comments kept)
99
103
  sqlplan.ts (sprint 92) EXPLAIN (FORMAT JSON) as a tree: parsePlan() (self time, slowest step, misestimates), planText()
100
104
  quicksql.ts Quick SQL: shorthand parser and PostgreSQL DDL generator (unit tested)
@@ -110,7 +114,12 @@ src/
110
114
  geocode.ts the installation's geocoder for address items (GEOCODER_URL, Nominatim search API)
111
115
  mvt.ts Mapbox Vector Tile encoding for map layers served as tiles
112
116
  yamltext.ts the text style of the directory export: a strict YAML subset, written and read
113
- webclient.ts outgoing HTTP to web services: allow-list, address checks at connect time (SSRF), redirects, limits
117
+ webclient.ts outgoing HTTP to web services: allow-list, address checks at connect time (SSRF), redirects, limits;
118
+ (sprint 118) a request's scope (OutboundScope): the workspace's network access list, checked on every hop
119
+ (checkUrl/scopeProblem), refusals reported to the scope
120
+ network-acl.ts (sprint 118, WS-9) workspace network access lists (meta.workspace.network_*): scopeForApp/scopeForWorkspace
121
+ (no cache), parseHostList, recordRefusal/purgeRefusals (meta.network_refusal, 7 days), mailHostAddress
122
+ (a workspace mail service's SMTP host: both lists, resolved once, the checked address connected to)
114
123
  datapump.ts (sprint 103, DA-16) Schema Export / Import jobs (meta.dump_job): findTools (pg_dump/pg_restore ≥ the
115
124
  server's major), startExport (pg_dump, argument array, PGPASSWORD), readToc/parseToc (pg_restore --list),
116
125
  startImport (pg_restore -f - run in one transaction between schema renames; data-only in FK order),
@@ -137,7 +146,10 @@ src/
137
146
  or XOAUTH2, Microsoft Graph sendMail, Amazon SES v2 raw MIME with signV4, Resend, Postmark, SendGrid,
138
147
  Mailgun, log; MailError says whether a retry may help; client-credentials token cache; settingsProblems),
139
148
  mail.ts (sendWith(): service enabled, allowed sender, secret decrypted; mailTick(): claim with SKIP LOCKED,
140
- send, retry after 1/4/16/64 minutes, purge after 30 days; the NOTIFY pgkiln_mail listener)
149
+ send, retry after 1/4/16/64 minutes, purge after 30 days; the NOTIFY pgkiln_mail listener);
150
+ (sprint 118) the service of meta.mail_service_for_app (the app's, else its workspace's own), each attempt's
151
+ provider and duration, serviceSettings()/verifyService(): a workspace service's checks (no base or token URL,
152
+ its SMTP host through network-acl.ts mailHostAddress)
141
153
  ai/ AI services (migration 060): types.ts (the provider interface, AiError kinds),
142
154
  anthropic.ts (Claude through @anthropic-ai/sdk: streamed, effort, structured outputs via
143
155
  output_config.format, refusal fallbacks, stop_reason checks, typed SDK errors), openai.ts (the openai
@@ -164,7 +176,8 @@ src/
164
176
  shortcuts.ts (103) "NAME" shortcuts in static content, help texts and confirm messages
165
177
  render.ts page chrome (nav, breadcrumb), dynamic action JSON, theme (the page's nonce'd <style>, light/dark and style switches)
166
178
  styles.ts base styles (BASE_STYLES: Iris, Lavender, Standard; baseStyleOf → html data-style), Theme Roller style variants: fixed lists (fonts, sizes, corners), parseStyle/appStyles checks, the request's
167
- style (user choice, default), themeCss() (only hex values and constants reach the CSS)
179
+ style (user choice, default), themeCss() (only hex values and constants reach the CSS); (sprint 117) NAV_APPEARANCES
180
+ (Pillar Light / Dark), navAppearance() → the <body> classes nav-pillar-*, the header strip (--strip)
168
181
  template-options.ts template options: the fixed CSS class list per region and button, templateClasses() (unknown values ignored)
169
182
  regions.ts region shell + chart (drill-down links, gauge settings)/cards/dynamic dispatch with row limits, lazy placeholder and cache, buttons (menu buttons, badges; (124) menu entries {action} → data-menu-entry for menu_entry dynamic actions)
170
183
  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 (facetChart: a facet's counts as a chart, sprint 65), items.ts
@@ -253,7 +266,10 @@ src/
253
266
  wizards.ts create page wizards: step 2 forms per page type (defaults from meta.wizard_defaults), POST → meta.generate_page
254
267
  (the generators are PL/pgSQL in migration 047: catalog, defaults, form/cards/calendar/chart/map/facets/master-detail)
255
268
  home.ts App Builder home at /builder/apps and /builder (tiles, applications report/cards, Recent; /builder redirects
256
- to the Workspace home when meta.workspace.builder_home = 'home'), Create, Import, Dashboard, Utilities
269
+ to the Workspace home when meta.workspace.builder_home = 'home'), Create, Import, Dashboard, Utilities;
270
+ (116) Updated by, working copies and the application lock in the report and the cards
271
+ app-updates.ts (sprint 116, migration 125) who changed an application last: developer() notes a builder POST under
272
+ /apps/:id or /pages/:pid (or a Create / Import), an onSend hook records a success in meta.app_change
257
273
  workspace-home.ts (sprint 109, migration 119) the Workspace home /builder/home, opened after sign-in: tool cards (Data Reporter
258
274
  when GET /builder/reporter exists), workspace message edited in place (workspaceAdmin), Summary, Top
259
275
  applications / users / developers over meta.developer.home_timeframe, System message (src/release-check.ts),
@@ -286,7 +302,15 @@ src/
286
302
  (Workspace home). Every id from a request is looked up in the current workspace
287
303
  region-help.ts the Attributes (JSON) help per region type, with links to the guide
288
304
  mail.ts Workspace utilities → Mail services (administrators: providers, write-only secret, allowed senders,
289
- applications with From and daily limit, test) and Applications → Activity → Mail (retry, cancel)
305
+ applications with From and daily limit, test) and Applications → Activity → Mail (retry, cancel);
306
+ serviceForm/formValues/runCheck shared with workspace-mail.ts (sprint 118)
307
+ mail-queue.ts (sprint 118, WS-9) Administration → Mail queue (/builder/mail-queue): queueScope (administrators: all or
308
+ ?ws=; workspace administrators: the current workspace), figures, per hour, per workspace / application,
309
+ slowest provider calls, failures; Retry / Purge one or all failed, scope checked again per mail
310
+ network-access.ts (sprint 118, WS-9) Administration → Network access (/builder/network): the installation's list, a workspace's
311
+ list (workspaceAdmin), Check a URL (nothing called), the workspaces (administrators), refused calls
312
+ workspace-mail.ts (sprint 118, WS-9) a workspace's own mail service (/builder/workspaces/:id/mail, WORKSPACE_<id>): its
313
+ administrators only, write-only secret, checks through mail.ts runCheck with the workspace's guard
290
314
  shared.ts Shared Components and access control
291
315
  designer.ts page designer: component tree (with computations and branches), layout canvas and gallery, property editor, toolbar;
292
316
  (sprint 86) GET /builder/apps/:app/pages/:no, /builder/pages/:id redirects browsers there (designerUrl in ui.ts)
@@ -304,6 +328,7 @@ src/
304
328
  object-browser.ts (sprint 75) the Object Browser: objects by type, system objects hidden, a table in tabs, column actions as SQL to review
305
329
  table-editor.ts (sprint 95) Table Properties: GET/POST /sql/objects/table (create, edit ?o=, ?template=), fromForm(), Preview / Apply
306
330
  (one transaction) / Open in the worksheet
331
+ data-security.ts (sprint 119) a table's Security: GET/POST /sql/objects/security?o= (policies, roles × columns, masks), Preview / Apply
307
332
  object-actions.ts (sprint 94) an object's ⋯ menu (objectMenu) in the worksheet and the Object Browser: Quick DDL (objectDdl), Drop with
308
333
  dependents, Add index / trigger / policy as drafts; GET /sql/objects/action, /sql/objects/panel (Open in a panel)
309
334
  data-modeler.ts (sprint 98) SQL Workshop → Data Modeler: /sql/modeler (diagrams, meta.sql_diagram, 111), /:id (SVG), /:id/objects, /layout,
@@ -348,7 +373,8 @@ src/
348
373
  ldap.ts Users → LDAP directories
349
374
  documents.ts document template preview (Shared Components)
350
375
  pwa.ts Settings → Progressive Web App (icon upload)
351
- themeroller.ts Settings → Theme Roller: style variants (add, edit, rename, delete), default style, users may choose
376
+ themeroller.ts Settings → Theme Roller: style variants (add, edit, rename, delete), default style, users may choose;
377
+ (sprint 117) navigation appearance and header strip colours (application and per style), the preview's menu
352
378
  subscriptions.ts Shared Components → Subscriptions: subscribe, refresh, unsubscribe, subscribers and publish; the note under a component
353
379
  reporter.ts page designer: Data Reporter settings (sources: table or view, offered columns, labels, masks; sharing;
354
380
  (sprint 110) a dataset as a source, who may edit, who may moderate, publishing)
@@ -396,7 +422,8 @@ src/
396
422
  into agents and regions), toolCallTable() (Activity → AI usage → Tool calls), agentOptions()
397
423
  supporting.ts supporting objects: review page, running the install/upgrade/deinstall scripts as the app's role in one transaction
398
424
  public/
399
- app.css theme (light/dark, responsive; --font, --font-size, --radius for style variants; template option classes to-*)
425
+ app.css theme (light/dark, responsive; --font, --font-size, --radius for style variants; template option classes to-*;
426
+ (sprint 117) navigation tree states, RTL via logical properties, body.nav-pillar-*, u-color-*-subtle, the busy bar)
400
427
  app.js client runtime: dialogs (dialog_closed actions), popup LOVs (sprint 115: the next page loads on scroll through an
401
428
  IntersectionObserver, ‹/More without one), dynamic actions (focus, classes, messages; sprint 115: wait_popup /
402
429
  remove_wait_popup as a modal <dialog class="wait-popup">, pgkiln.waitPopup(), the menu_entry event), grids (add/duplicate rows, master-detail refresh, move/resize columns, copy/paste of cell ranges), menus, lazy regions, maps (Leaflet layers, marker clusters, heat layer, layer legend, area/distance filter) (no inline JS)
@@ -455,6 +482,8 @@ test/
455
482
  page-logic.test.ts download, chain (background jobs) and workflow processes, function/app branches, dialog_closed (HR page 28)
456
483
  code-editor.test.ts code editor: completions scoped to the app's role, the check, marked fields
457
484
  builder-home.test.ts App Builder home: search, sort, views, Recent, Create/Import pages, dashboard, utilities
485
+ app-list.test.ts (116) the App Builder list's Updated by, working copies and lock; what counts as a change
486
+ import-only.test.ts (116) pgkiln import --replace --only: a page or component file, refusals
458
487
  charts.test.ts chart markup per kind (geometry as classes), gauges, Gantt time axis and dependencies, pyramid, polar, range, box plot, stock, drill-down links
459
488
  calendar.test.ts calendar views, create links, moving events (pure and over HTTP)
460
489
  rest-sources.test.ts REST data sources, web credentials, SSRF checks, invoke_api (mock service + HR page 23)
@@ -473,7 +502,11 @@ test/
473
502
  builder-parity.test.ts lists (HR page 31), page and application locks, comments, developers, supporting objects
474
503
  page-wizards.test.ts create page wizards: catalog defaults, every page type generated and rendered, refusals, the builder steps
475
504
  theme-styles.test.ts Theme Roller style variants (checks, CSS, user choice per app, builder page), template options, base style Iris
505
+ theme-pillar.test.ts (sprint 117) Pillar Light / Dark appearances and the header strip: checks, CSS, runtime classes, Theme Roller, export
476
506
  workspaces.test.ts workspaces (064): Default, administrators' pages, current workspace, refused apps, imports and copies
507
+ mail-network.test.ts (sprint 118) network access lists (page process refused and logged in debug, narrowing only, redirects,
508
+ the page), a workspace's mail service (sending, SMTP host on the list), the Mail queue (yesterday's
509
+ SMTP failure, Retry, Purge, counts, scope)
477
510
  gallery.test.ts the App Gallery: every starter created from its review with all its pages rendering; sample apps (admins)
478
511
  builder-apex-screens.test.ts sprint 61: sign-in with a workspace, message and banner, utilization, SQL Workshop home, groups, reports
479
512
  workspace-isolation.test.ts isolated workspaces (090): logins, RLS on meta, app_id, roles, SQL Workshop, scripts, imports
@@ -493,6 +526,8 @@ test/
493
526
  ai-mock.ts a local mock of the Claude Messages API (SSE stream) and OpenAI Chat Completions
494
527
  helpers.ts a cookie-keeping test browser
495
528
  e2e/responsive.test.ts browser tests at phone/tablet/desktop widths (Playwright)
529
+ e2e/admin-mail-network.test.ts (sprint 118) Mail queue, Network access and a workspace's mail service at 390 px without JavaScript
530
+ e2e/pillar.test.ts (sprint 117) Pillar appearances: axe in light and dark, subtle colours, current vs parent entry, Arabic RTL, 390 px, preview
496
531
  e2e/code-editor.test.ts the code editor in a browser: highlighting, keys, suggestions, touch, screen readers, syntax colours at AA in both themes (sprint 108)
497
532
  e2e/data-modeler.test.ts (sprint 98) the Data Modeler in a browser: drag, save, reload, undo/redo, zoom, mini-map, exports, phone
498
533
  e2e/worksheet.test.ts (sprint 91) the SQL Worksheet in a browser: run keys, tabs, navigator, drag and drop, splitters, phone, no JavaScript;
@@ -245,6 +245,8 @@ application. The base style and colours under Settings → Theme are the *Standa
245
245
  |---|---|
246
246
  | Accent colour, header colour | `#rrggbb` (or "as the base"), for the light theme |
247
247
  | Accent and header colour in dark mode | `#rrggbb` (or the base style's dark palette), for the dark theme; choose light colours (text on the accent is dark there) |
248
+ | Navigation appearance | *As the application*, *Default* (the base style's header and menu), *Pillar Light* or *Pillar Dark*, see [below](#navigation-appearances-pillar-light-and-pillar-dark) |
249
+ | Header strip colour, also for dark mode | `#rrggbb` or an item reference (else the application's strip, else the accent) |
248
250
  | Font | *System* (default), *Humanist sans*, *Geometric sans*, *Serif*, *Rounded*, *Monospace* (font stacks of fonts on the device; nothing is downloaded) |
249
251
  | Font size | 14, 15 (default), 16 or 17 px |
250
252
  | Corners | Square, 4, 8 (default) or 14 px, for regions, buttons and fields |
@@ -276,10 +278,56 @@ per tenant, environment or user group:
276
278
  a style themselves get the first style (in the list's order) whose condition holds, else the
277
279
  default style. The condition runs as the application's database role in the page's transaction;
278
280
  an error counts as *false* and is written to the [debug messages](06-processing.md#debug-messages).
279
- - **Dynamic colours.** Instead of a picked colour, any of a style's four colours may be an item
281
+ - **Dynamic colours.** Instead of a picked colour, any of a style's colours may be an item
280
282
  reference such as `&APP_BRAND_COLOUR.`: the item's value in session state (set by a computation,
281
283
  an application process after login, a form…), used when it is a `#rrggbb` colour and ignored
282
284
  otherwise, so a stored value can never become anything but a colour in the CSS.
283
285
 
284
286
  Together: one style *Tenant brand* with condition `:APP_BRAND_COLOUR is not null` and accent
285
287
  `&APP_BRAND_COLOUR.` gives each tenant its colour from a table, read once after sign-in.
288
+
289
+ ## Navigation appearances: Pillar Light and Pillar Dark
290
+
291
+ APEX 26.2's Universal Theme adds *Pillar* appearances for the header and the navigation menu. In pgkiln:
292
+ **Theme Roller → Navigation appearance** (for the whole application) or a style's own *Navigation appearance*:
293
+
294
+ | Appearance | Look |
295
+ |---|---|
296
+ | **Default** | The base style's header and menu (Iris: a dark header and menu; Standard: a navy header and a white menu) |
297
+ | **Pillar Light** | Header and menu on the page's own light surfaces (in dark mode its dark ones), a line under the header |
298
+ | **Pillar Dark** | Header and menu as one dark column in the header colour; in dark mode a darker one |
299
+
300
+ Both have a **header pillar strip** across the top of the header, in the *Header strip colour* (light and dark mode,
301
+ on the Theme Roller page or per style; a style may take it from an item, like its other colours), else the accent,
302
+ and show the **current page as a filled pill** in the accent. Pillar Light replaces the header colour with the page's
303
+ surface, so the strip carries the brand colour. A style's appearance wins over the application's; *Default* in a style
304
+ switches a pillar appearance off. The appearance works with a side and a top menu, and on tablets and phones in the
305
+ drawer. It is stored as `"appearance"`, `"strip"` and `"strip_dark"` in `meta.app.theme` (and in each style), so it
306
+ travels with export and import; only the two keys of the list and `#rrggbb` colours reach the page (classes
307
+ `nav-pillar nav-pillar-light` / `nav-pillar-dark` on `<body>`, `--strip` in the nonce'd `<style>`).
308
+
309
+ ### The navigation tree
310
+
311
+ In every appearance (sprint 117) the menu tells three states apart: the **current page** is filled, in the accent,
312
+ with a bar at the start of the entry (a pill in the pillar appearances); its **parent entries** (the trail) are bold
313
+ with an accent icon but not filled, and the open parent's sub-menu hangs on an accent guide line; the **selected**
314
+ entry (keyboard focus) has a ring inside the menu. A page that is not in the menu marks its nearest menu ancestor as
315
+ the trail.
316
+
317
+ ### Right to left
318
+
319
+ Arabic, Hebrew, Persian and Urdu pages (`dir="rtl"`) mirror the chrome: the menu sits on the right, the drawer
320
+ slides in from the right on tablets and phones, the current page's bar, the guide lines and the menu's chevrons turn
321
+ around, menus open from the other side and reflowed report cards are aligned to the start.
322
+
323
+ ### Subtle colour utilities and the processing indicator
324
+
325
+ The classes `u-color-1-subtle` … `u-color-15-subtle` and `u-color-success-subtle`, `u-color-danger-subtle`,
326
+ `u-color-warning-subtle`, `u-color-info-subtle`, `u-color-neutral-subtle` give an element a tinted background with
327
+ readable text (WCAG AA in light and dark mode, in every base style), for static content, HTML expressions and
328
+ template components, e.g. `<span class="u-color-4-subtle">Approved</span>`.
329
+
330
+ A region that is busy (a dynamic action, a refresh) is dimmed slightly after 150 ms, so quick answers don't flicker,
331
+ and shows a thin moving bar along its top (a still bar when the device asks for reduced motion); the wait popup's
332
+ spinner is lighter too.
333
+
@@ -35,7 +35,7 @@ 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] [--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)) |
38
+ | `pgkiln import <path> [--alias <alias>] [--replace [--only <file>]…] [--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)); with `--only pages/12` (repeatable) only that page or component file ([below](#importing-one-page-or-component)). `--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
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
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 |
@@ -210,6 +210,26 @@ pgkiln diff hr apps/hr # on the target: what would cha
210
210
  pgkiln import apps/hr --replace # deploy
211
211
  ```
212
212
 
213
+ ### Importing one page or component
214
+
215
+ (sprint 116; APEX 26.2: APEXlang file-level import) With `--replace`, `--only` imports just the named files of a
216
+ directory export (or a .zip of one) and leaves the rest of the application as it is in the database:
217
+
218
+ ```sh
219
+ pgkiln import apps/hr --replace --only pages/12 # page 12 with its regions, items, buttons …
220
+ pgkiln import apps/hr --replace --only shared/lovs/jobs.yaml # one shared component
221
+ pgkiln import apps/hr --replace --only pages/12 --only shared/lovs # several: repeat --only
222
+ ```
223
+
224
+ `pages/12` (or just `12`) is page 12's folder whatever its name, so a renamed page still matches; any other value is
225
+ a file or folder path inside the export. A page or file that the directory no longer has is removed from the
226
+ application. How it works: pgkiln exports the application in the directory's style, takes the named files from the
227
+ directory, and imports the result with `--replace` as usual: one transaction, a backup first, `--check` and
228
+ `--dry-run` work the same, and what belongs to the installation is kept. It says how many of the named files differ
229
+ (on standard error). `--only` refuses without `--replace`, for a JSON file, for a path outside the export and for a
230
+ selection that matches nothing in the directory or the application (exit code 2). A component that another file
231
+ refers to by name (a list of values used on a page, say) has to exist after the import, as with any import.
232
+
213
233
  ### Checking before an import
214
234
 
215
235
  An import doesn't need the application's tables, columns and functions to exist (neither does APEX's):
@@ -53,6 +53,16 @@ The checks on every outgoing request (`src/webclient.ts`), against server-side r
53
53
  Keep the allow-list as short as you can. `*` lets developers call any public host from the
54
54
  server; it never opens private addresses.
55
55
 
56
+ **A workspace's network access list** (sprint 118; APEX 26.2 workspace network ACLs) narrows this
57
+ list for one workspace: under Administration → [Network access](03-builder.md#network-access), its
58
+ administrators can limit its applications to some hosts (same entry syntax, without `*`). A call made
59
+ for an application of that workspace must then match the workspace's list *and* the installation's,
60
+ on every redirect hop; the address checks above stay with `PGKILN_REST_PRIVATE_HOSTS` alone, so a
61
+ workspace can never widen what the installation allows. It applies to every path above: REST data
62
+ sources (including write-back, synchronisation and the response cache), `meta.web_request`, invoke
63
+ API, token endpoints, object storage and REST source catalogs. Refusals are logged in the
64
+ application's debug log and listed under *Refused calls*.
65
+
56
66
  **Changing `PGKILN_SECRET_KEY`** makes the stored secrets unreadable: pgkiln then reports that
57
67
  the secret must be entered again. Keep the key with your other server secrets, outside the
58
68
  database: a database dump alone does not reveal the secrets.
@@ -323,6 +333,8 @@ values, processes and workflow steps refer to sources and credentials by name, s
323
333
 
324
334
  - Outgoing calls are off until the administrator sets `PGKILN_REST_ALLOWED_HOSTS`; private and
325
335
  internal addresses need `PGKILN_REST_PRIVATE_HOSTS` as well.
336
+ - A workspace's [network access list](03-builder.md#network-access) narrows the allow-list for its
337
+ applications (it can't widen it).
326
338
  - Secrets are encrypted with a key outside the database, write-only in the builder, excluded from
327
339
  exports and from the runtime role, and never logged. Use **Valid for URLs** so a credential is
328
340
  only ever sent to its own service.
@@ -52,7 +52,7 @@ directory the agent started the server in (the project).
52
52
  | `describe_schema` | schemas with tables, views and functions; or one table with columns, constraints, indexes, RLS policies and triggers | no |
53
53
  | `generate_ddl` | the DDL of a schema's objects, in an order that runs: a start for a migration script | no |
54
54
  | `compare_schemas` | the differences between two schemas and a script that makes the target like the source (not run) | no |
55
- | `run_query` | one SQL statement, read-only, rows as JSON | no |
55
+ | `run_query` | one SQL statement, read-only, rows as JSON; with `alias` (and `user`) as that application's role and user | no |
56
56
  | `search_docs` | searches this user guide, the parity matrix and SECURITY.md | no |
57
57
  | `recent_errors` | the latest errors and refused requests from the activity log | no |
58
58
  | `export_app` | writes an application to a directory (default `apps/<alias>`, format `text`) | files on disk |
@@ -85,7 +85,13 @@ migration scripts, not in the application files (`run_query` can't change them a
85
85
  columns whose names contain *password*, *secret*, *token*, *hash* or *api_key* are shown as
86
86
  `(hidden)`. That keeps credentials out of the agent's context by accident; it is not an access
87
87
  control: the server runs as the owner role, like the command line, and is meant for a
88
- developer's own machine. Don't point it at production; deploy with `pgkiln import --replace`
88
+ developer's own machine.
89
+ - **As an application's user.** (sprint 119) `run_query` with `alias` runs the statement as that application's
90
+ database role, with `meta.app_id()`, `meta.app_user()` (`user`, default `nobody`) and `meta.has_role()` (the
91
+ user's roles in the application) set, so grants, column privileges, row level security and masking views apply as
92
+ on that user's pages ([chapter 8](08-security.md#data-security-per-table-rows-columns-and-values)). Use it to check
93
+ what a user sees, or let an agent look at data the way the application does. It is no sandbox either: the
94
+ connection is still the owner's. Don't point it at production; deploy with `pgkiln import --replace`
89
95
  from git instead.
90
96
  - **Writes** are only `export_app` (files; it refuses a non-empty directory that isn't an export,
91
97
  so it can't overwrite your project) and `import_app` (the database, in one transaction). Both are
@@ -0,0 +1,79 @@
1
+ -- =====================================================================
2
+ -- HR example, part 70: data security per table (sprint 119, WS-13;
3
+ -- docs/guide/08-security.md "Data security per table")
4
+ --
5
+ -- hr.emp_masked, the masking view that SQL Workshop › hr.emp › Security
6
+ -- writes when sal and comm are masked (empty) for everyone but the
7
+ -- application role manager. It runs as hr_masking, so row level security
8
+ -- on hr.emp would still apply inside it. hr_app keeps SELECT on hr.emp
9
+ -- because the other pages read it: the Security page warns that the mask
10
+ -- holds only where the view is read.
11
+ --
12
+ -- Page 48 "Salaries (masked)": a report on the view. Signed in as KING
13
+ -- (manager) it shows salaries; as ALLEN (no manager role) they are empty.
14
+ -- =====================================================================
15
+
16
+ do $role$ begin
17
+ if not exists (select from pg_roles where rolname = 'hr_masking') then
18
+ create role hr_masking nologin;
19
+ end if;
20
+ end $role$;
21
+ grant hr_masking to current_user;
22
+ grant usage on schema hr to hr_masking;
23
+ grant select on hr.emp to hr_masking;
24
+ grant hr_app to hr_masking;
25
+
26
+ create view hr.emp_masked with (security_barrier) as
27
+ select t.empno,
28
+ t.ename,
29
+ t.job,
30
+ t.mgr,
31
+ t.hiredate,
32
+ case when (select meta.has_role('manager')) then t.sal end as sal,
33
+ case when (select meta.has_role('manager')) then t.comm end as comm,
34
+ t.deptno,
35
+ t.active,
36
+ t.username,
37
+ t.photo,
38
+ t.photo_name,
39
+ t.photo_mime,
40
+ t.work_location
41
+ from hr.emp t;
42
+ alter view hr.emp_masked owner to hr_masking;
43
+ comment on view hr.emp_masked is 'pgkiln masking view of emp: {"table":"emp","roles":["manager"],"cols":{"comm":{"mask":"null","see":["manager"],"others":false},"sal":{"mask":"null","see":["manager"],"others":false}}}';
44
+ grant select on hr.emp_masked to hr_app;
45
+
46
+ do $check$ begin
47
+ if exists (select from pg_roles r where r.rolname = 'hr_masking'
48
+ and (r.rolsuper or r.rolbypassrls or pg_has_role(r.oid, (select relowner from pg_class where oid = 'hr.emp'::regclass), 'USAGE'))) then
49
+ raise exception 'the masking role hr_masking would bypass row level security on hr.emp';
50
+ end if;
51
+ end $check$;
52
+
53
+ insert into meta.page (app_id, page_no, name, title, parent_page)
54
+ select id, 48, 'Salaries (masked)', 'Salaries (masked)', 1 from meta.app where alias = 'hr';
55
+
56
+ insert into meta.region (page_id, seq, title, type, columns, template, source, config)
57
+ select p.id, r.seq, r.title, r.type, 12, 'standard', r.source, r.config::jsonb
58
+ from meta.page p join meta.app a on a.id = p.app_id, (values
59
+ (10, 'Salaries (masked)', 'static',
60
+ '<p>This report reads <code>hr.emp_masked</code>: salary and commission are only shown to the role <b>manager</b>. Open SQL Workshop › hr.emp › Security to change who sees what.</p>', '{}'),
61
+ (20, 'Employees', 'report',
62
+ 'select ename, job, sal, comm from hr.emp_masked where active order by ename',
63
+ '{"page_size": 15, "headings": {"ename": "Name", "job": "Job", "sal": "Salary", "comm": "Commission"}}')
64
+ ) as r (seq, title, type, source, config)
65
+ where a.alias = 'hr' and p.page_no = 48;
66
+
67
+ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
68
+ select id, 35, 'Salaries (masked)', 'shield', 48 from meta.app where alias = 'hr';
69
+
70
+ insert into meta.translation (app_id, language, source, target)
71
+ select a.id, 'nl', t.source, t.target
72
+ from meta.app a, (values
73
+ ('Salaries (masked)', 'Salarissen (gemaskeerd)'),
74
+ ('Employees', 'Medewerkers'),
75
+ ('Salary', 'Salaris'),
76
+ ('Commission', 'Commissie')
77
+ ) as t(source, target)
78
+ where a.alias = 'hr'
79
+ on conflict do nothing;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",