pgkiln 0.31.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) 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/docs/apex-feature-parity.md +55 -35
  15. package/docs/guide/01-installation.md +1 -0
  16. package/docs/guide/03-builder.md +118 -2
  17. package/docs/guide/04-pages-and-regions.md +115 -0
  18. package/docs/guide/05-items.md +47 -0
  19. package/docs/guide/06-processing.md +210 -2
  20. package/docs/guide/07-dynamic-actions.md +60 -2
  21. package/docs/guide/08-security.md +13 -2
  22. package/docs/guide/09-reference.md +21 -0
  23. package/docs/guide/11-from-apex.md +5 -1
  24. package/docs/guide/12-development.md +33 -1
  25. package/docs/guide/14-globalization.md +15 -0
  26. package/docs/guide/15-extensions.md +1 -1
  27. package/docs/guide/18-cli.md +30 -1
  28. package/docs/guide/20-ai-agents.md +3 -2
  29. package/examples/hr/hr_50_search.sql +47 -0
  30. package/examples/hr/hr_51_mail.sql +70 -0
  31. package/examples/hr/hr_52_feedback.sql +15 -0
  32. package/examples/hr/hr_53_data_settings.sql +108 -0
  33. package/package.json +3 -1
  34. package/public/app.css +62 -0
  35. package/public/app.js +408 -11
  36. package/public/builder.css +11 -0
  37. package/public/builder.js +27 -0
  38. package/src/app.ts +8 -0
  39. package/src/appfiles.ts +12 -0
  40. package/src/automations.ts +12 -0
  41. package/src/builder/advisor.ts +54 -14
  42. package/src/builder/arrange.ts +2 -2
  43. package/src/builder/availability.ts +74 -0
  44. package/src/builder/backups.ts +134 -0
  45. package/src/builder/components.ts +109 -15
  46. package/src/builder/designer.ts +8 -3
  47. package/src/builder/feedback.ts +82 -0
  48. package/src/builder/forms.ts +4 -1
  49. package/src/builder/home.ts +3 -0
  50. package/src/builder/locks.ts +15 -5
  51. package/src/builder/mail.ts +425 -0
  52. package/src/builder/newapp.ts +1 -1
  53. package/src/builder/region-help.ts +71 -0
  54. package/src/builder/routes.ts +44 -1
  55. package/src/builder/search.ts +4 -4
  56. package/src/builder/shared.ts +3 -2
  57. package/src/builder/ui.ts +7 -1
  58. package/src/cli/apps.ts +46 -5
  59. package/src/cli/main.ts +21 -5
  60. package/src/cli/mcp.ts +16 -4
  61. package/src/cli/replace.ts +36 -4
  62. package/src/geocode.ts +44 -0
  63. package/src/i18n/ar.ts +39 -0
  64. package/src/i18n/cs.ts +39 -0
  65. package/src/i18n/da.ts +39 -0
  66. package/src/i18n/de.ts +39 -0
  67. package/src/i18n/el.ts +39 -0
  68. package/src/i18n/es.ts +39 -0
  69. package/src/i18n/fi.ts +39 -0
  70. package/src/i18n/fr.ts +39 -0
  71. package/src/i18n/he.ts +39 -0
  72. package/src/i18n/it.ts +39 -0
  73. package/src/i18n/ja.ts +39 -0
  74. package/src/i18n/ko.ts +39 -0
  75. package/src/i18n/nb.ts +39 -0
  76. package/src/i18n/pl.ts +39 -0
  77. package/src/i18n/pt.ts +39 -0
  78. package/src/i18n/ru.ts +39 -0
  79. package/src/i18n/sv.ts +39 -0
  80. package/src/i18n/tr.ts +39 -0
  81. package/src/i18n/uk.ts +39 -0
  82. package/src/i18n/zh.ts +39 -0
  83. package/src/i18n.ts +78 -0
  84. package/src/mail/mail.ts +148 -0
  85. package/src/mail/providers.ts +444 -0
  86. package/src/metadata.ts +26 -8
  87. package/src/runtime/ajax.ts +97 -0
  88. package/src/runtime/authz.ts +1 -1
  89. package/src/runtime/availability.ts +37 -0
  90. package/src/runtime/context.ts +46 -6
  91. package/src/runtime/data-export.ts +94 -0
  92. package/src/runtime/data-items.ts +194 -0
  93. package/src/runtime/devbar.ts +182 -0
  94. package/src/runtime/engine.ts +26 -1
  95. package/src/runtime/feedback.ts +128 -0
  96. package/src/runtime/items.ts +8 -0
  97. package/src/runtime/processes.ts +83 -2
  98. package/src/runtime/regions.ts +8 -0
  99. package/src/runtime/render.ts +33 -7
  100. package/src/runtime/rest.ts +4 -0
  101. package/src/runtime/routes.ts +59 -5
  102. package/src/runtime/search.ts +252 -0
  103. package/src/runtime/url-region.ts +93 -0
  104. package/src/security.ts +11 -1
  105. package/src/server.ts +2 -0
  106. package/src/session.ts +8 -2
  107. package/src/workingcopy.ts +2 -2
@@ -0,0 +1,73 @@
1
+ -- =====================================================================
2
+ -- 082: automatic application backups (APEX: Application Backups)
3
+ --
4
+ -- meta.app_backup keeps exports of an application: one before every
5
+ -- import that replaces it (pgkiln import --replace, the MCP import, a
6
+ -- working copy's merge, a restore), one a day when it changed since the
7
+ -- last backup (the scheduler), and the ones a developer makes. The newest
8
+ -- meta.app.backups_keep (default 25, 0 = none) are kept per application.
9
+ -- The builder lists, downloads, restores and deletes them (Application →
10
+ -- Backups); a restore is an import --replace of the backup, so the version
11
+ -- it replaces is backed up too. Installation data: not exported, kept by
12
+ -- an import over the application.
13
+ -- =====================================================================
14
+
15
+ create table meta.app_backup (
16
+ id bigint generated always as identity primary key,
17
+ app_id int not null references meta.app on delete cascade,
18
+ created_at timestamptz not null default now(),
19
+ created_by text,
20
+ reason text not null check (reason in ('import', 'merge', 'restore', 'daily', 'manual')),
21
+ note text check (length(note) <= 500),
22
+ doc jsonb not null,
23
+ -- md5 of the export, so a daily backup of an unchanged application is skipped
24
+ doc_hash text not null,
25
+ size int not null
26
+ );
27
+ create index app_backup_app on meta.app_backup (app_id, created_at desc);
28
+ revoke all on meta.app_backup from public;
29
+ comment on table meta.app_backup is 'Application backups (082): exports kept by this installation, never exported';
30
+
31
+ alter table meta.app add column backups_keep int not null default 25 check (backups_keep between 0 and 500);
32
+
33
+ -- an export made before 082 has no backups_keep: NULL becomes 25
34
+ create function meta.app_backups_keep_default() returns trigger
35
+ language plpgsql set search_path = pg_catalog as $$
36
+ begin
37
+ new.backups_keep := coalesce(new.backups_keep, 25);
38
+ return new;
39
+ end $$;
40
+ create trigger app_backups_keep before insert or update on meta.app
41
+ for each row execute function meta.app_backups_keep_default();
42
+
43
+ -- Back an application up (the server and the command line call it as the owner). A daily
44
+ -- backup of an application that didn't change since its newest backup is skipped (NULL).
45
+ create function meta.backup_app(p_app_id int, p_reason text, p_user text default null, p_note text default null)
46
+ returns bigint
47
+ language plpgsql volatile set search_path = meta, pg_catalog as $$
48
+ declare
49
+ v_alias text;
50
+ v_keep int;
51
+ v_doc jsonb;
52
+ v_hash text;
53
+ v_id bigint;
54
+ begin
55
+ select alias, backups_keep into v_alias, v_keep from meta.app where id = p_app_id;
56
+ if v_alias is null or v_keep = 0 then
57
+ return null;
58
+ end if;
59
+ v_doc := meta.export_app(v_alias);
60
+ v_hash := md5(v_doc::text);
61
+ if p_reason = 'daily' and v_hash = (select doc_hash from meta.app_backup where app_id = p_app_id order by created_at desc, id desc limit 1) then
62
+ return null;
63
+ end if;
64
+ insert into meta.app_backup (app_id, created_by, reason, note, doc, doc_hash, size)
65
+ values (p_app_id, p_user, p_reason, left(p_note, 500), v_doc, v_hash, octet_length(v_doc::text))
66
+ returning id into v_id;
67
+ delete from meta.app_backup
68
+ where app_id = p_app_id
69
+ and id not in (select id from meta.app_backup where app_id = p_app_id order by created_at desc, id desc limit v_keep);
70
+ return v_id;
71
+ end
72
+ $$;
73
+ revoke all on function meta.backup_app(int, text, text, text) from public;
@@ -0,0 +1,156 @@
1
+ -- =====================================================================
2
+ -- 083: application availability and build status (APEX: Application
3
+ -- Availability and Build Status)
4
+ --
5
+ -- meta.app.availability:
6
+ -- available everyone who may sign in (the default)
7
+ -- developers only developers with a builder session in the same browser
8
+ -- (the developer toolbar's token, migration 079)
9
+ -- restricted only the users in availability_users (and developers)
10
+ -- unavailable nobody: every page shows availability_message (or a default)
11
+ -- Pages, file downloads, REST modules and PostgREST (meta.api_check) follow
12
+ -- it; automations, workflows, background jobs and queued mail keep running.
13
+ --
14
+ -- meta.app.build_status 'run_only': the builder refuses changes to the
15
+ -- application (as if it were locked), except to its availability and build
16
+ -- status. The command line's import --replace (deployment) still works.
17
+ --
18
+ -- Both belong to the installation: export_app leaves them out (redefined from
19
+ -- 081), an import of a new application gets the defaults, and an import over
20
+ -- an application keeps its values (src/cli/replace.ts).
21
+ -- =====================================================================
22
+
23
+ alter table meta.app
24
+ add column availability text not null default 'available' check (availability in ('available', 'developers', 'restricted', 'unavailable')),
25
+ add column availability_message text check (length(availability_message) <= 2000),
26
+ add column availability_users text[] not null default '{}',
27
+ add column build_status text not null default 'run_and_build' check (build_status in ('run_and_build', 'run_only'));
28
+
29
+ -- an export has none of these: NULL becomes the default
30
+ create function meta.app_availability_default() returns trigger
31
+ language plpgsql set search_path = pg_catalog as $$
32
+ begin
33
+ new.availability := coalesce(new.availability, 'available');
34
+ new.availability_users := coalesce(new.availability_users, '{}');
35
+ new.build_status := coalesce(new.build_status, 'run_and_build');
36
+ return new;
37
+ end $$;
38
+ create trigger app_availability before insert or update on meta.app
39
+ for each row execute function meta.app_availability_default();
40
+
41
+ comment on column meta.app.availability is 'available, developers, restricted or unavailable (083)';
42
+ comment on column meta.app.build_status is 'run_and_build, or run_only: the builder refuses changes (083)';
43
+
44
+ -- ---------------------------------------------------------------- export
45
+ -- Same as 081, without the availability and build status.
46
+ create or replace function meta.export_app(p_alias text) returns jsonb
47
+ language sql stable set search_path = meta, pg_catalog as $$
48
+ select jsonb_build_object(
49
+ 'format', 'pgkiln/2',
50
+ -- (083) availability and build status belong to the installation
51
+ 'app', to_jsonb(a) - 'id' - 'created_at' - 'updated_at' - 'availability' - 'availability_message' - 'availability_users' - 'build_status',
52
+ 'authz_schemes', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.authz_scheme x where x.app_id = a.id), '[]'),
53
+ 'app_items', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.app_item x where x.app_id = a.id), '[]'),
54
+ 'app_processes', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.seq, x.id) from meta.app_process x where x.app_id = a.id), '[]'),
55
+ 'lovs', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.lov x where x.app_id = a.id), '[]'),
56
+ 'group_roles', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.group_name, x.role) from meta.app_group_role x where x.app_id = a.id), '[]'),
57
+ 'text_messages', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.name, x.language) from meta.text_message x where x.app_id = a.id), '[]'),
58
+ 'translations', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.language, x.source) from meta.translation x where x.app_id = a.id), '[]'),
59
+ 'report_layouts', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'logo' || jsonb_build_object('logo', encode(x.logo, 'base64')) order by x.name)
60
+ from meta.report_layout x where x.app_id = a.id), '[]'),
61
+ 'automations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'next_run_at' - 'last_run_at' - 'last_status' - 'code' order by x.name)
62
+ from meta.automation x where x.app_id = a.id), '[]'),
63
+ -- (044) the actions of the automations, by automation name
64
+ 'automation_actions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.automation_name collate "C", x.seq, x.name collate "C")
65
+ from meta.automation_action x where x.app_id = a.id), '[]'),
66
+ 'document_templates', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.document_template x where x.app_id = a.id), '[]'),
67
+ 'task_definitions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.task_definition x where x.app_id = a.id), '[]'),
68
+ 'workflow_definitions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.workflow_definition x where x.app_id = a.id), '[]'),
69
+ 'rest_modules', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.rest_module x where x.app_id = a.id), '[]'),
70
+ 'template_components', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.static_id) from meta.template_component x where x.app_id = a.id), '[]'),
71
+ 'build_options', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.build_option x where x.app_id = a.id), '[]'),
72
+ -- (030) secrets never leave the installation: they are entered again after an import
73
+ -- (050) nor do the password and refresh token of the OAuth2 flows, or the token state
74
+ 'web_credentials', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at' order by x.name)
75
+ from meta.web_credential x where x.app_id = a.id), '[]'),
76
+ -- (050) the synchronisation's state (next and last run) belongs to the installation
77
+ 'rest_sources', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'sync_next_at' - 'sync_last_at' - 'sync_last_status' order by x.name)
78
+ from meta.rest_source x where x.app_id = a.id), '[]'),
79
+ -- (041) data load definitions
80
+ 'data_load_definitions', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.data_load_def x where x.app_id = a.id), '[]'),
81
+ -- (042) lists; entries keep their ids so parents can be remapped on import
82
+ 'lists', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.list x where x.app_id = a.id), '[]'),
83
+ 'list_entries', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.list_name, x.parent_id nulls first, x.seq, x.id) from meta.list_entry x where x.app_id = a.id), '[]'),
84
+ -- (042) supporting objects: install, upgrade and deinstall scripts (never run on import)
85
+ 'supporting_scripts', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.supporting_script x where x.app_id = a.id), '[]'),
86
+ -- (069) plug-ins (their files are static files, their templates template components)
87
+ 'plugins', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.plugin x where x.app_id = a.id), '[]'),
88
+ -- (080) search configurations
89
+ 'search_configs', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.search_config x where x.app_id = a.id), '[]'),
90
+ -- (081) e-mail templates
91
+ 'email_templates', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.email_template x where x.app_id = a.id), '[]'),
92
+ -- (068) static application files (JavaScript, CSS, images, fonts) as base64
93
+ 'static_files', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'content' - 'updated_at' || jsonb_build_object('content', translate(encode(x.content, 'base64'), E'\n', '')) order by x.name)
94
+ from meta.static_file x where x.app_id = a.id), '[]'),
95
+ -- nav entries and regions keep their ids, so parents and references can be remapped on import
96
+ 'nav', coalesce((select jsonb_agg(to_jsonb(x) - 'app_id' order by x.parent_id nulls first, x.seq, x.id) from meta.nav_entry x where x.app_id = a.id), '[]'),
97
+ 'pages', coalesce((
98
+ select jsonb_agg(to_jsonb(p) - 'id' - 'app_id' || jsonb_build_object(
99
+ 'regions', coalesce((select jsonb_agg(to_jsonb(r) - 'page_id' order by r.seq, r.id) from meta.region r where r.page_id = p.id), '[]'),
100
+ 'items', coalesce((select jsonb_agg(to_jsonb(i) - 'id' - 'page_id' order by i.seq, i.id) from meta.item i where i.page_id = p.id), '[]'),
101
+ 'buttons', coalesce((select jsonb_agg(to_jsonb(b) - 'id' - 'page_id' order by b.seq, b.id) from meta.button b where b.page_id = p.id), '[]'),
102
+ 'dynamic_actions', coalesce((select jsonb_agg(to_jsonb(d) - 'id' - 'page_id' order by d.seq, d.id) from meta.dynamic_action d where d.page_id = p.id), '[]'),
103
+ 'validations', coalesce((select jsonb_agg(to_jsonb(v) - 'id' - 'page_id' order by v.seq, v.id) from meta.validation v where v.page_id = p.id), '[]'),
104
+ 'processes', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'page_id' order by x.seq, x.id) from meta.process x where x.page_id = p.id), '[]'),
105
+ 'computations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'page_id' order by x.seq, x.id) from meta.computation x where x.page_id = p.id), '[]'),
106
+ 'branches', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'page_id' order by x.seq, x.id) from meta.branch x where x.page_id = p.id), '[]')
107
+ ) order by p.page_no)
108
+ from meta.page p where p.app_id = a.id), '[]'))
109
+ from meta.app a
110
+ where a.alias = p_alias
111
+ $$;
112
+
113
+ -- ---------------------------------------------------------------- PostgREST
114
+ -- Same as 011, plus the application's availability.
115
+ create or replace function meta.api_check() returns void
116
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
117
+ declare
118
+ v_claims jsonb := meta.jwt_claims();
119
+ v_role text := current_setting('role', true); -- the SET ROLE PostgREST did
120
+ v_app meta.app;
121
+ v_acc meta.account;
122
+ begin
123
+ if v_claims is null or v_role is null or v_role in ('none', 'pgkiln_anon') then
124
+ return;
125
+ end if;
126
+ select * into v_app from meta.app a where a.alias = v_claims->>'app';
127
+ if not found or v_app.api_role is distinct from v_role then
128
+ raise exception 'This token is not valid for an application with API role %.', v_role
129
+ using errcode = 'PT401';
130
+ end if;
131
+ -- (083) the application's availability: unavailable or for developers only answers 503;
132
+ -- restricted only for the users it names (never for an OAuth client)
133
+ if v_app.availability in ('unavailable', 'developers') then
134
+ raise exception '%', coalesce(nullif(v_app.availability_message, ''), 'The application is not available at the moment.') using errcode = 'PT503';
135
+ end if;
136
+ if v_app.availability = 'restricted'
137
+ and (v_claims ? 'client_id' or not lower(meta.app_user()) = any (select lower(u) from unnest(v_app.availability_users) u)) then
138
+ raise exception 'The application is not available to this account at the moment.' using errcode = 'PT403';
139
+ end if;
140
+ if v_claims ? 'client_id' then
141
+ if not exists (select 1 from meta.api_client c
142
+ where c.client_id = v_claims->>'client_id' and c.app_id = v_app.id and c.active) then
143
+ raise exception 'The OAuth client of this token does not exist or was revoked.' using errcode = 'PT401';
144
+ end if;
145
+ return;
146
+ end if;
147
+ select * into v_acc from meta.account ac where lower(ac.username) = lower(meta.app_user());
148
+ if not found or not v_acc.active then
149
+ raise exception 'The account of this token does not exist or is inactive.' using errcode = 'PT403';
150
+ end if;
151
+ if v_app.access_control <> 'any_user'
152
+ and not exists (select 1 from meta.app_access aa where aa.app_id = v_app.id and aa.account_id = v_acc.id) then
153
+ raise exception 'The account of this token has no access to the application.' using errcode = 'PT403';
154
+ end if;
155
+ end
156
+ $$;
@@ -0,0 +1,46 @@
1
+ -- =====================================================================
2
+ -- 084: feedback (APEX: Feedback, Team Development → Feedback)
3
+ --
4
+ -- With meta.app.feedback on, the application's navigation bar has a
5
+ -- "Feedback" link: users send a comment, an idea or a problem (with an
6
+ -- optional rating) about the page they came from, and see their earlier
7
+ -- feedback with the developers' answers. Developers read, answer and
8
+ -- close it in the builder (Application → Feedback). The setting travels
9
+ -- with the export; the feedback itself is installation data.
10
+ -- =====================================================================
11
+
12
+ alter table meta.app add column feedback boolean not null default false;
13
+
14
+ -- an export made before 084 has no feedback setting: NULL becomes false
15
+ create function meta.app_feedback_default() returns trigger
16
+ language plpgsql set search_path = pg_catalog as $$
17
+ begin
18
+ new.feedback := coalesce(new.feedback, false);
19
+ return new;
20
+ end $$;
21
+ create trigger app_feedback before insert or update on meta.app
22
+ for each row execute function meta.app_feedback_default();
23
+
24
+ create table meta.feedback (
25
+ id bigint generated always as identity primary key,
26
+ app_id int not null references meta.app on delete cascade,
27
+ page_no int,
28
+ username text not null,
29
+ session_id uuid,
30
+ kind text not null check (kind in ('comment', 'idea', 'problem')),
31
+ rating smallint check (rating between 1 and 5),
32
+ body text not null check (length(body) between 1 and 4000),
33
+ user_agent text check (length(user_agent) <= 300),
34
+ status text not null default 'open' check (status in ('open', 'in_progress', 'done', 'declined')),
35
+ response text check (length(response) <= 4000),
36
+ responded_by text,
37
+ responded_at timestamptz,
38
+ created_at timestamptz not null default now()
39
+ );
40
+ create index feedback_app on meta.feedback (app_id, created_at desc);
41
+ revoke all on meta.feedback from public;
42
+ -- the runtime records feedback and shows users their own (src/runtime/feedback.ts); developers answer
43
+ grant select, insert on meta.feedback to pgkiln_runtime;
44
+
45
+ comment on table meta.feedback is 'Feedback of application users and the developers'' answers (084); not exported';
46
+ comment on column meta.app.feedback is 'Users can send feedback from the navigation bar (084)';
@@ -0,0 +1,65 @@
1
+ -- =====================================================================
2
+ -- 085: page behaviour (sprint 52)
3
+ --
4
+ -- Dynamic actions (APEX: events and actions that were missing):
5
+ -- events after_refresh (a region was refreshed or loaded; trigger: region
6
+ -- ids or static ids), selection_change (rows selected in a report
7
+ -- or grid; trigger: region ids or static ids), focus and blur (Get /
8
+ -- Lose Focus of items), key_release (items), before_submit (the page
9
+ -- is about to be submitted; a declined Confirm stops it), custom
10
+ -- (trigger: event names, raised with pgkiln.trigger(name, value))
11
+ -- actions confirm (a question; No stops the actions after it), close_dialog
12
+ -- (close the modal page and refresh its parent), cancel_dialog,
13
+ -- clear (empty the affected items), open_region / close_region,
14
+ -- download (runs a download process of the page by name), print,
15
+ -- current_position (latitude,longitude into the affected item),
16
+ -- share (the device's share sheet, or the link copied)
17
+ -- The actions of one event and trigger now run one after the other, in
18
+ -- sequence order (APEX: the true actions of a dynamic action).
19
+ --
20
+ -- Ajax callbacks (APEX: On Demand processes, apex.server.process): page and
21
+ -- application processes with point "ajax" run only when JavaScript calls
22
+ -- them by name: POST /a/<alias>/<page>/ajax/<name> (src/runtime/ajax.ts).
23
+ --
24
+ -- meta.app.error_function (APEX: Error Handling Function): a function of the
25
+ -- application, function(jsonb) returns jsonb, that may rewrite every error
26
+ -- message shown to users (src/runtime/context.ts publicError).
27
+ --
28
+ -- meta.app.session_warning_seconds (APEX: Session timeout warning): seconds
29
+ -- before the session ends that the page warns and offers to stay signed in
30
+ -- (0: no warning).
31
+ -- =====================================================================
32
+
33
+ alter table meta.dynamic_action drop constraint dynamic_action_event_check;
34
+ alter table meta.dynamic_action add constraint dynamic_action_event_check check (event in (
35
+ 'change', 'click', 'load', 'dialog_closed', 'after_refresh', 'selection_change', 'focus', 'blur', 'key_release', 'before_submit', 'custom'));
36
+
37
+ alter table meta.dynamic_action drop constraint dynamic_action_action_check;
38
+ alter table meta.dynamic_action add constraint dynamic_action_action_check check (action in (
39
+ 'show', 'hide', 'enable', 'disable', 'set_value', 'execute_sql', 'refresh_region', 'refresh_item', 'alert', 'submit',
40
+ 'set_focus', 'add_class', 'remove_class', 'show_success', 'show_error', 'clear_errors', 'ai_generate', 'execute_javascript', 'plugin',
41
+ 'push_subscribe', 'confirm', 'close_dialog', 'cancel_dialog', 'clear', 'open_region', 'close_region', 'download', 'print',
42
+ 'current_position', 'share'));
43
+
44
+ alter table meta.process drop constraint process_point_check;
45
+ alter table meta.process add constraint process_point_check check (point in ('submit', 'load', 'ajax'));
46
+ alter table meta.app_process drop constraint app_process_point_check;
47
+ alter table meta.app_process add constraint app_process_point_check check (point in ('after_login', 'before_page', 'ajax'));
48
+
49
+ alter table meta.app
50
+ -- schema.function: called as the application's role with a jsonb error, returns jsonb (or null)
51
+ add column error_function text check (error_function ~ '^[A-Za-z_][A-Za-z0-9_$]{0,62}\.[A-Za-z_][A-Za-z0-9_$]{0,62}$'),
52
+ add column session_warning_seconds int not null default 120 check (session_warning_seconds between 0 and 3600);
53
+
54
+ -- an export made before 085 has no session warning: NULL becomes 120
55
+ create function meta.app_session_warning_default() returns trigger
56
+ language plpgsql set search_path = pg_catalog as $$
57
+ begin
58
+ new.session_warning_seconds := coalesce(new.session_warning_seconds, 120);
59
+ return new;
60
+ end $$;
61
+ create trigger app_session_warning before insert or update on meta.app
62
+ for each row execute function meta.app_session_warning_default();
63
+
64
+ comment on column meta.app.error_function is 'schema.function(jsonb) returns jsonb that rewrites error messages (085)';
65
+ comment on column meta.app.session_warning_seconds is 'Warn this many seconds before the session ends; 0: no warning (085)';