pgkiln 0.35.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 (76) hide show
  1. package/db/migrations/087_login_page.sql +175 -0
  2. package/db/migrations/088_region_templates.sql +18 -0
  3. package/db/migrations/089_page_groups.sql +330 -0
  4. package/docs/apex-feature-parity.md +22 -20
  5. package/docs/guide/03-builder.md +36 -2
  6. package/docs/guide/04-pages-and-regions.md +42 -3
  7. package/docs/guide/08-security.md +46 -5
  8. package/docs/guide/11-from-apex.md +4 -1
  9. package/docs/guide/12-development.md +7 -1
  10. package/docs/guide/17-mobile.md +3 -1
  11. package/docs/guide/18-cli.md +2 -0
  12. package/docs/guide/20-ai-agents.md +2 -0
  13. package/examples/hr/hr_54_region_templates.sql +67 -0
  14. package/examples/hr/hr_55_page_groups.sql +24 -0
  15. package/package.json +1 -1
  16. package/public/app.css +60 -3
  17. package/public/app.js +89 -3
  18. package/public/builder.css +11 -0
  19. package/public/builder.js +50 -0
  20. package/public/code-editor.css +3 -3
  21. package/public/sw.js +5 -3
  22. package/src/app.ts +2 -0
  23. package/src/appfiles.ts +1 -0
  24. package/src/builder/advisor.ts +14 -0
  25. package/src/builder/components.ts +15 -2
  26. package/src/builder/ddl-tools.ts +114 -0
  27. package/src/builder/designer.ts +29 -3
  28. package/src/builder/forms.ts +6 -4
  29. package/src/builder/login-page.ts +82 -0
  30. package/src/builder/region-help.ts +4 -2
  31. package/src/builder/region-settings.ts +19 -3
  32. package/src/builder/routes.ts +16 -4
  33. package/src/builder/shared.ts +1 -1
  34. package/src/builder/sql.ts +3 -0
  35. package/src/builder/ui.ts +2 -2
  36. package/src/cli/main.ts +45 -0
  37. package/src/cli/mcp.ts +42 -0
  38. package/src/cli/replace.ts +4 -0
  39. package/src/ddl.ts +509 -0
  40. package/src/i18n/ar.ts +17 -0
  41. package/src/i18n/cs.ts +17 -0
  42. package/src/i18n/da.ts +17 -0
  43. package/src/i18n/de.ts +17 -0
  44. package/src/i18n/el.ts +17 -0
  45. package/src/i18n/es.ts +17 -0
  46. package/src/i18n/fi.ts +17 -0
  47. package/src/i18n/fr.ts +17 -0
  48. package/src/i18n/he.ts +17 -0
  49. package/src/i18n/it.ts +17 -0
  50. package/src/i18n/ja.ts +17 -0
  51. package/src/i18n/ko.ts +17 -0
  52. package/src/i18n/nb.ts +17 -0
  53. package/src/i18n/pl.ts +17 -0
  54. package/src/i18n/pt.ts +17 -0
  55. package/src/i18n/ru.ts +17 -0
  56. package/src/i18n/sv.ts +17 -0
  57. package/src/i18n/tr.ts +17 -0
  58. package/src/i18n/uk.ts +17 -0
  59. package/src/i18n/zh.ts +17 -0
  60. package/src/i18n.ts +34 -0
  61. package/src/icons.ts +2 -1
  62. package/src/layout.ts +3 -2
  63. package/src/metadata.ts +10 -4
  64. package/src/runtime/devbar.ts +3 -1
  65. package/src/runtime/display-selector.ts +12 -4
  66. package/src/runtime/grid.ts +170 -26
  67. package/src/runtime/lists.ts +11 -1
  68. package/src/runtime/password-reset.ts +171 -0
  69. package/src/runtime/pwa.ts +7 -10
  70. package/src/runtime/regions.ts +33 -0
  71. package/src/runtime/render.ts +15 -0
  72. package/src/runtime/report-views.ts +4 -2
  73. package/src/runtime/report.ts +36 -22
  74. package/src/runtime/routes.ts +35 -32
  75. package/src/runtime/signin.ts +80 -0
  76. package/src/static-version.ts +34 -0
@@ -0,0 +1,175 @@
1
+ -- =====================================================================
2
+ -- 087: an editable login page and "Forgot password?" for app users
3
+ --
4
+ -- meta.app.login_page: the page that replaces the built-in sign-in page
5
+ -- (APEX: the login page, usually 9999). /a/<alias>/login sends visitors
6
+ -- there; it must not require authentication. Its "login" region is the
7
+ -- sign-in form, which posts to /a/<alias>/login as before.
8
+ -- meta.page.page_template: 'minimal' shows a page without the header and
9
+ -- navigation (APEX: Minimal (No Navigation)); login pages use it.
10
+ -- meta.app.password_reset: the sign-in page offers "Forgot password?" to
11
+ -- accounts with a password in the user directory, when the application
12
+ -- has an enabled mail service. A reset link is single-use and expires
13
+ -- after 30 minutes; only a SHA-256 hash of its token is stored.
14
+ -- meta.password_reset: the tokens (owner only; the functions below are
15
+ -- the runtime's way in).
16
+ -- =====================================================================
17
+ alter table meta.app add column login_page int check (login_page > 0);
18
+ alter table meta.app add column password_reset boolean not null default false;
19
+ alter table meta.page add column page_template text not null default 'standard' check (page_template in ('standard', 'minimal'));
20
+
21
+ -- an export made before 087 has neither: NULL becomes the default (like 077)
22
+ create function meta.login_page_defaults() returns trigger
23
+ language plpgsql set search_path = pg_catalog as $$
24
+ begin
25
+ if tg_table_name = 'app' then
26
+ new.password_reset := coalesce(new.password_reset, false);
27
+ else
28
+ new.page_template := coalesce(new.page_template, 'standard');
29
+ end if;
30
+ return new;
31
+ end
32
+ $$;
33
+ create trigger app_login_page_defaults before insert or update on meta.app
34
+ for each row execute function meta.login_page_defaults();
35
+ create trigger page_login_page_defaults before insert or update on meta.page
36
+ for each row execute function meta.login_page_defaults();
37
+
38
+ alter table meta.region drop constraint region_type_check;
39
+ alter table meta.region add constraint region_type_check
40
+ check (type in ('report', 'form', 'chart', 'cards', 'static', 'grid', 'calendar', 'dynamic', 'facets', 'tasks', 'workflows', 'map', 'tree',
41
+ 'template_component', 'smart_filters', 'display_selector', 'list', 'data_reporter', 'ai_assistant', 'plugin', 'url', 'search',
42
+ 'login'));
43
+
44
+ create table meta.password_reset (
45
+ id bigint generated always as identity primary key,
46
+ app_id int not null references meta.app on delete cascade,
47
+ account_id int not null references meta.account on delete cascade,
48
+ token_hash text not null unique check (token_hash ~ '^[0-9a-f]{64}$'),
49
+ ip text,
50
+ created_at timestamptz not null default now(),
51
+ expires_at timestamptz not null default now() + interval '30 minutes',
52
+ used_at timestamptz
53
+ );
54
+ create index password_reset_account on meta.password_reset (account_id, created_at);
55
+ revoke all on meta.password_reset from public;
56
+
57
+ -- Whether the application offers "Forgot password?": turned on, app users with local passwords,
58
+ -- and an enabled mail service with a From address.
59
+ create function meta.password_reset_available(p_app_id int) returns boolean
60
+ language sql stable security definer set search_path = meta, pg_catalog as $$
61
+ select exists (
62
+ select 1
63
+ from meta.app a
64
+ join meta.app_mail_service ams on ams.app_id = a.id
65
+ join meta.mail_service s on s.id = ams.service_id
66
+ where a.id = p_app_id and a.password_reset and a.authentication = 'app_users' and a.local_login
67
+ and s.enabled and nullif(btrim(ams.from_address), '') is not null)
68
+ $$;
69
+ revoke all on function meta.password_reset_available(int) from public;
70
+ grant execute on function meta.password_reset_available(int) to pgkiln_runtime;
71
+
72
+ -- A reset request for a user name or e-mail address. When it matches one active account with a
73
+ -- password and an e-mail address that may use the application, keeps the token's hash and queues
74
+ -- the mail (#USER# in the subject and bodies becomes the user name; the caller puts the link in).
75
+ -- Says nothing about whether an account matched: the caller shows the same answer either way.
76
+ -- At most 3 requests per account per hour.
77
+ create function meta.request_password_reset(p_app_id int, p_login text, p_token_hash text, p_ip text,
78
+ p_subject text, p_body text, p_body_html text) returns void
79
+ language plpgsql volatile security definer set search_path = meta, public, pg_catalog as $$
80
+ declare
81
+ v_acc meta.account;
82
+ v_n int;
83
+ v_from text;
84
+ v_send text[];
85
+ v_max int;
86
+ v_vals jsonb;
87
+ v_id bigint;
88
+ begin
89
+ if not meta.password_reset_available(p_app_id) or p_token_hash !~ '^[0-9a-f]{64}$' or btrim(coalesce(p_login, '')) = '' then
90
+ return;
91
+ end if;
92
+ -- a user name, else an e-mail address that only one active account has
93
+ select * into v_acc from meta.account where lower(username) = lower(btrim(p_login)) and active;
94
+ if not found then
95
+ select count(*) into v_n from meta.account where lower(email) = lower(btrim(p_login)) and active;
96
+ if v_n <> 1 then
97
+ return;
98
+ end if;
99
+ select * into v_acc from meta.account where lower(email) = lower(btrim(p_login)) and active;
100
+ end if;
101
+ if v_acc.password_hash is null or nullif(btrim(v_acc.email), '') is null then
102
+ return;
103
+ end if;
104
+ if not exists (select 1 from meta.app a where a.id = p_app_id and a.access_control = 'any_user')
105
+ and not exists (select 1 from meta.app_access where app_id = p_app_id and account_id = v_acc.id) then
106
+ return;
107
+ end if;
108
+ select count(*) into v_n from meta.password_reset where account_id = v_acc.id and created_at > now() - interval '1 hour';
109
+ if v_n >= 3 then
110
+ return;
111
+ end if;
112
+ select ams.from_address, s.senders, ams.max_per_day into v_from, v_send, v_max
113
+ from meta.app_mail_service ams join meta.mail_service s on s.id = ams.service_id
114
+ where ams.app_id = p_app_id;
115
+ if not meta.mail_sender_allowed(v_send, v_from) then
116
+ raise warning 'password reset for app %: the mail service may not send as %', p_app_id, v_from;
117
+ return;
118
+ end if;
119
+ if v_max is not null and (select count(*) from meta.mail_queue
120
+ where app_id = p_app_id and created_at >= date_trunc('day', now() at time zone 'UTC') at time zone 'UTC'
121
+ and status <> 'cancelled') >= v_max then
122
+ raise warning 'password reset for app %: the daily mail limit is reached', p_app_id;
123
+ return;
124
+ end if;
125
+ delete from meta.password_reset where created_at < now() - interval '7 days';
126
+ insert into meta.password_reset (app_id, account_id, token_hash, ip) values (p_app_id, v_acc.id, p_token_hash, left(p_ip, 100));
127
+ v_vals := jsonb_build_object('USER', v_acc.username);
128
+ insert into meta.mail_queue (app_id, username, from_address, to_addresses, subject, body_text, body_html, template)
129
+ values (p_app_id, v_acc.username, v_from, array[v_acc.email], left(replace(replace(meta.mail_fill(p_subject, v_vals, false), E'\r', ''), E'\n', ' '), 998),
130
+ meta.mail_fill(p_body, v_vals, false), meta.mail_fill(p_body_html, v_vals, true), 'PASSWORD_RESET')
131
+ returning id into v_id;
132
+ perform pg_notify('pgkiln_mail', v_id::text);
133
+ end
134
+ $$;
135
+ revoke all on function meta.request_password_reset(int, text, text, text, text, text, text) from public;
136
+ grant execute on function meta.request_password_reset(int, text, text, text, text, text, text) to pgkiln_runtime;
137
+
138
+ -- The user name a valid (unused, unexpired) token is for, or null.
139
+ create function meta.password_reset_user(p_app_id int, p_token_hash text) returns text
140
+ language sql stable security definer set search_path = meta, pg_catalog as $$
141
+ select ac.username
142
+ from meta.password_reset r join meta.account ac on ac.id = r.account_id
143
+ where r.app_id = p_app_id and r.token_hash = p_token_hash and r.used_at is null and r.expires_at > now() and ac.active
144
+ $$;
145
+ revoke all on function meta.password_reset_user(int, text) from public;
146
+ grant execute on function meta.password_reset_user(int, text) to pgkiln_runtime;
147
+
148
+ -- Set the new password with a valid token: the token and the account's other open requests are used
149
+ -- up, every session and "keep me signed in" token of the account ends. Returns the user name, or
150
+ -- null when the token is not valid (any more).
151
+ create function meta.reset_password(p_app_id int, p_token_hash text, p_new text) returns text
152
+ language plpgsql volatile security definer set search_path = meta, public, pg_catalog as $$
153
+ declare
154
+ v_reset meta.password_reset;
155
+ v_acc meta.account;
156
+ begin
157
+ select * into v_reset from meta.password_reset
158
+ where app_id = p_app_id and token_hash = p_token_hash and used_at is null and expires_at > now()
159
+ for update;
160
+ if not found or coalesce(p_new, '') = '' then
161
+ return null;
162
+ end if;
163
+ select * into v_acc from meta.account where id = v_reset.account_id and active;
164
+ if not found then
165
+ return null;
166
+ end if;
167
+ update meta.account set password_hash = crypt(p_new, gen_salt('bf', 10)), must_change_password = false where id = v_acc.id;
168
+ update meta.password_reset set used_at = now() where account_id = v_acc.id and used_at is null;
169
+ delete from meta.session where app_id is not null and lower(username) = lower(v_acc.username);
170
+ delete from meta.persistent_login where account_id = v_acc.id;
171
+ return v_acc.username;
172
+ end
173
+ $$;
174
+ revoke all on function meta.reset_password(int, text, text) from public;
175
+ grant execute on function meta.reset_password(int, text, text) to pgkiln_runtime;
@@ -0,0 +1,18 @@
1
+ -- =====================================================================
2
+ -- 088: more region templates (APEX: Hero, Title Bar, Buttons Container,
3
+ -- Inline Dialog, Inline Drawer, Wizard Container)
4
+ --
5
+ -- hero a large banner: title, text, an icon, buttons
6
+ -- title_bar the title and buttons on one line, without a card
7
+ -- buttons_container the buttons only (and any text), without a header
8
+ -- inline_dialog hidden until opened (dynamic action "Open region", or
9
+ -- a link to #R<id>); a dialog over the page
10
+ -- inline_drawer the same, sliding in from the side
11
+ -- wizard a narrow card for one step of a wizard (with the list
12
+ -- template "wizard_progress" above it)
13
+ -- The carousel is a style of the display selector region ({"style":
14
+ -- "carousel"}); it needs no column.
15
+ -- =====================================================================
16
+ alter table meta.region drop constraint region_template_check;
17
+ alter table meta.region add constraint region_template_check
18
+ check (template in ('standard', 'plain', 'collapsible', 'hero', 'title_bar', 'buttons_container', 'inline_dialog', 'inline_drawer', 'wizard'));
@@ -0,0 +1,330 @@
1
+ -- =====================================================================
2
+ -- 089: page groups (APEX: Shared Components → Page Groups)
3
+ --
4
+ -- meta.page_group: an application's named groups of pages, to organise
5
+ -- them in the App Builder (the page list by group, a filter). A page's
6
+ -- group is meta.page.page_group (the group's name; renaming a group in
7
+ -- the builder renames it on its pages). They change nothing at run time.
8
+ -- export_app / import_app are redefined from 086 to carry page_groups.
9
+ -- =====================================================================
10
+ create table meta.page_group (
11
+ id serial primary key,
12
+ app_id int not null references meta.app on delete cascade,
13
+ name text not null check (name ~ '^[A-Za-z0-9][A-Za-z0-9 _.-]{0,59}$'),
14
+ description text,
15
+ unique (app_id, name)
16
+ );
17
+ revoke all on meta.page_group from public;
18
+ grant select on meta.page_group to pgkiln_runtime;
19
+ comment on table meta.page_group is 'Page groups (089): named groups of an application''s pages, for the App Builder';
20
+
21
+ alter table meta.page add column page_group text;
22
+
23
+ -- renaming a group renames it on its pages; deleting it takes the pages out of it
24
+ create function meta.page_group_follow() returns trigger
25
+ language plpgsql set search_path = meta, pg_catalog as $$
26
+ begin
27
+ if tg_op = 'DELETE' then
28
+ update meta.page set page_group = null where app_id = old.app_id and page_group = old.name;
29
+ return old;
30
+ end if;
31
+ if new.name is distinct from old.name then
32
+ update meta.page set page_group = new.name where app_id = new.app_id and page_group = old.name;
33
+ end if;
34
+ return new;
35
+ end
36
+ $$;
37
+ create trigger page_group_follow after update or delete on meta.page_group
38
+ for each row execute function meta.page_group_follow();
39
+
40
+ create or replace function meta.export_app(p_alias text) returns jsonb
41
+ language sql stable set search_path = meta, pg_catalog as $$
42
+ select jsonb_build_object(
43
+ 'format', 'pgkiln/2',
44
+ -- (083) availability and build status belong to the installation
45
+ 'app', to_jsonb(a) - 'id' - 'created_at' - 'updated_at' - 'availability' - 'availability_message' - 'availability_users' - 'build_status',
46
+ '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), '[]'),
47
+ '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), '[]'),
48
+ '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), '[]'),
49
+ '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), '[]'),
50
+ '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), '[]'),
51
+ '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), '[]'),
52
+ '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), '[]'),
53
+ '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)
54
+ from meta.report_layout x where x.app_id = a.id), '[]'),
55
+ 'automations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'next_run_at' - 'last_run_at' - 'last_status' - 'code' order by x.name)
56
+ from meta.automation x where x.app_id = a.id), '[]'),
57
+ -- (044) the actions of the automations, by automation name
58
+ '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")
59
+ from meta.automation_action x where x.app_id = a.id), '[]'),
60
+ '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), '[]'),
61
+ '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), '[]'),
62
+ '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), '[]'),
63
+ '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), '[]'),
64
+ '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), '[]'),
65
+ '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), '[]'),
66
+ -- (030) secrets never leave the installation: they are entered again after an import
67
+ -- (050) nor do the password and refresh token of the OAuth2 flows, or the token state
68
+ '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)
69
+ from meta.web_credential x where x.app_id = a.id), '[]'),
70
+ -- (050) the synchronisation's state (next and last run) belongs to the installation
71
+ '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)
72
+ from meta.rest_source x where x.app_id = a.id), '[]'),
73
+ -- (041) data load definitions
74
+ '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), '[]'),
75
+ -- (042) lists; entries keep their ids so parents can be remapped on import
76
+ '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), '[]'),
77
+ '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), '[]'),
78
+ -- (042) supporting objects: install, upgrade and deinstall scripts (never run on import)
79
+ '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), '[]'),
80
+ -- (069) plug-ins (their files are static files, their templates template components)
81
+ '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), '[]'),
82
+ -- (080) search configurations
83
+ '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), '[]'),
84
+ -- (081) e-mail templates
85
+ '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), '[]'),
86
+ -- (086) application settings (with their values) and dynamic translations
87
+ 'app_settings', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.app_setting x where x.app_id = a.id), '[]'),
88
+ 'page_groups', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.name) from meta.page_group x where x.app_id = a.id), '[]'),
89
+ 'dynamic_translations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' order by x.language, x.source collate "C") from meta.dynamic_translation x where x.app_id = a.id), '[]'),
90
+ -- (068) static application files (JavaScript, CSS, images, fonts) as base64
91
+ '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)
92
+ from meta.static_file x where x.app_id = a.id), '[]'),
93
+ -- nav entries and regions keep their ids, so parents and references can be remapped on import
94
+ '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), '[]'),
95
+ 'pages', coalesce((
96
+ select jsonb_agg(to_jsonb(p) - 'id' - 'app_id' || jsonb_build_object(
97
+ '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), '[]'),
98
+ '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), '[]'),
99
+ '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), '[]'),
100
+ '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), '[]'),
101
+ '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), '[]'),
102
+ '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), '[]'),
103
+ '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), '[]'),
104
+ '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), '[]')
105
+ ) order by p.page_no)
106
+ from meta.page p where p.app_id = a.id), '[]'))
107
+ from meta.app a
108
+ where a.alias = p_alias
109
+ $$;
110
+
111
+ -- ---------------------------------------------------------------- import
112
+ -- Same as 081, plus app_settings and dynamic_translations.
113
+
114
+ create or replace function meta.import_app(p_doc jsonb, p_alias text default null) returns int
115
+ language plpgsql set search_path = meta, pg_catalog as $$
116
+ declare
117
+ v_app_id int;
118
+ v_page_id int;
119
+ v_page jsonb;
120
+ v_e jsonb;
121
+ v_rmap jsonb;
122
+ v_nmap jsonb := '{}';
123
+ v_lmap jsonb := '{}';
124
+ v_new_id int;
125
+ begin
126
+ if p_doc->>'format' is distinct from 'pgkiln/2' then
127
+ raise exception 'unsupported export format %', coalesce(p_doc->>'format', '(none)');
128
+ end if;
129
+
130
+ insert into meta.app
131
+ select (jsonb_populate_record(null::meta.app, p_doc->'app' || jsonb_build_object(
132
+ 'id', nextval('meta.app_id_seq'),
133
+ 'alias', coalesce(p_alias, p_doc->'app'->>'alias'),
134
+ 'created_at', now(), 'updated_at', now()))).*
135
+ returning id into v_app_id;
136
+
137
+ insert into meta.authz_scheme
138
+ select (jsonb_populate_record(null::meta.authz_scheme, e || jsonb_build_object('id', nextval('meta.authz_scheme_id_seq'), 'app_id', v_app_id))).*
139
+ from jsonb_array_elements(coalesce(p_doc->'authz_schemes', '[]')) e;
140
+ insert into meta.app_item
141
+ select (jsonb_populate_record(null::meta.app_item, e || jsonb_build_object('id', nextval('meta.app_item_id_seq'), 'app_id', v_app_id))).*
142
+ from jsonb_array_elements(coalesce(p_doc->'app_items', '[]')) e;
143
+ insert into meta.app_process
144
+ select (jsonb_populate_record(null::meta.app_process, e || jsonb_build_object('id', nextval('meta.app_process_id_seq'), 'app_id', v_app_id))).*
145
+ from jsonb_array_elements(coalesce(p_doc->'app_processes', '[]')) e;
146
+ insert into meta.lov
147
+ select (jsonb_populate_record(null::meta.lov, e || jsonb_build_object('id', nextval('meta.lov_id_seq'), 'app_id', v_app_id))).*
148
+ from jsonb_array_elements(coalesce(p_doc->'lovs', '[]')) e;
149
+ insert into meta.app_group_role
150
+ select (jsonb_populate_record(null::meta.app_group_role, e || jsonb_build_object('app_id', v_app_id))).*
151
+ from jsonb_array_elements(coalesce(p_doc->'group_roles', '[]')) e;
152
+ insert into meta.text_message
153
+ select (jsonb_populate_record(null::meta.text_message, e || jsonb_build_object('app_id', v_app_id))).*
154
+ from jsonb_array_elements(coalesce(p_doc->'text_messages', '[]')) e;
155
+ insert into meta.translation
156
+ select (jsonb_populate_record(null::meta.translation, e || jsonb_build_object('app_id', v_app_id))).*
157
+ from jsonb_array_elements(coalesce(p_doc->'translations', '[]')) e;
158
+ insert into meta.report_layout
159
+ select (jsonb_populate_record(null::meta.report_layout, (e - 'logo') || jsonb_build_object(
160
+ 'id', nextval('meta.report_layout_id_seq'), 'app_id', v_app_id,
161
+ 'logo', null))).*
162
+ from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e;
163
+ update meta.report_layout l
164
+ set logo = decode(e->>'logo', 'base64')
165
+ from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e
166
+ where l.app_id = v_app_id and l.name = e->>'name' and e->>'logo' is not null;
167
+ -- imported automations start switched off: the copy must not run the original's jobs unasked.
168
+ -- (044) a file with actions: the automations' code is ignored; an older file: code becomes the single action
169
+ insert into meta.automation
170
+ select (jsonb_populate_record(null::meta.automation, '{"error_handling": "stop"}'::jsonb
171
+ || jsonb_strip_nulls(case when p_doc ? 'automation_actions' then e - 'code' else e end)
172
+ || jsonb_build_object('id', nextval('meta.automation_id_seq'), 'app_id', v_app_id, 'enabled', false))).*
173
+ from jsonb_array_elements(coalesce(p_doc->'automations', '[]')) e;
174
+ insert into meta.automation_action
175
+ select (jsonb_populate_record(null::meta.automation_action, '{"seq": 10}'::jsonb || jsonb_strip_nulls(e)
176
+ || jsonb_build_object('id', nextval('meta.automation_action_id_seq'), 'app_id', v_app_id))).*
177
+ from jsonb_array_elements(coalesce(p_doc->'automation_actions', '[]')) e;
178
+ insert into meta.document_template
179
+ select (jsonb_populate_record(null::meta.document_template, e || jsonb_build_object('id', nextval('meta.document_template_id_seq'), 'app_id', v_app_id))).*
180
+ from jsonb_array_elements(coalesce(p_doc->'document_templates', '[]')) e;
181
+ insert into meta.task_definition
182
+ select (jsonb_populate_record(null::meta.task_definition, e || jsonb_build_object('id', nextval('meta.task_definition_id_seq'), 'app_id', v_app_id))).*
183
+ from jsonb_array_elements(coalesce(p_doc->'task_definitions', '[]')) e;
184
+ insert into meta.workflow_definition
185
+ select (jsonb_populate_record(null::meta.workflow_definition, e || jsonb_build_object('id', nextval('meta.workflow_definition_id_seq'), 'app_id', v_app_id))).*
186
+ from jsonb_array_elements(coalesce(p_doc->'workflow_definitions', '[]')) e;
187
+ insert into meta.rest_module
188
+ select (jsonb_populate_record(null::meta.rest_module, e || jsonb_build_object('id', nextval('meta.rest_module_id_seq'), 'app_id', v_app_id))).*
189
+ from jsonb_array_elements(coalesce(p_doc->'rest_modules', '[]')) e;
190
+ -- (028) template components: regions and report columns refer to them by static id
191
+ insert into meta.template_component
192
+ select (jsonb_populate_record(null::meta.template_component, e || jsonb_build_object('id', nextval('meta.template_component_id_seq'), 'app_id', v_app_id))).*
193
+ from jsonb_array_elements(coalesce(p_doc->'template_components', '[]')) e;
194
+ -- (029) build options (components name them by name)
195
+ insert into meta.build_option
196
+ select (jsonb_populate_record(null::meta.build_option, e || jsonb_build_object('id', nextval('meta.build_option_id_seq'), 'app_id', v_app_id))).*
197
+ from jsonb_array_elements(coalesce(p_doc->'build_options', '[]')) e;
198
+ -- (030) web credentials arrive without a secret, whatever the document holds
199
+ -- (050) nor a password or refresh token
200
+ insert into meta.web_credential
201
+ select (jsonb_populate_record(null::meta.web_credential, '{"type": "basic", "valid_for": [], "grant_type": "client_credentials"}'::jsonb
202
+ || jsonb_strip_nulls(e - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at')
203
+ || jsonb_build_object('id', nextval('meta.web_credential_id_seq'), 'app_id', v_app_id))).*
204
+ from jsonb_array_elements(coalesce(p_doc->'web_credentials', '[]')) e;
205
+ -- (050) write-back operations and synchronisation; an imported synchronisation starts switched off
206
+ insert into meta.rest_source
207
+ select (jsonb_populate_record(null::meta.rest_source, '{"method": "GET", "headers": {}, "params": [], "columns": [], "cache_seconds": 0, "timeout_s": 10, "max_rows": 1000,
208
+ "operations": {}, "key_columns": [], "sync_mode": "merge", "sync_delete": false, "sync_time_zone": "UTC"}'::jsonb
209
+ || jsonb_strip_nulls(e - 'sync_next_at' - 'sync_last_at' - 'sync_last_status')
210
+ || jsonb_build_object('id', nextval('meta.rest_source_id_seq'), 'app_id', v_app_id, 'sync_enabled', false))).*
211
+ from jsonb_array_elements(coalesce(p_doc->'rest_sources', '[]')) e;
212
+
213
+ -- (041) data load definitions (the data_load process names them)
214
+ insert into meta.data_load_def
215
+ select (jsonb_populate_record(null::meta.data_load_def, '{"format": "auto", "headers": true, "mode": "append", "skip_errors": false, "columns": []}'::jsonb
216
+ || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.data_load_def_id_seq'), 'app_id', v_app_id))).*
217
+ from jsonb_array_elements(coalesce(p_doc->'data_load_definitions', '[]')) e;
218
+
219
+ -- (042) lists and their entries, supporting scripts. Entries are inserted
220
+ -- without a parent first and the parents set afterwards, so the order of
221
+ -- the entries in the document doesn't matter (a moved entry may have a
222
+ -- higher id than its children).
223
+ insert into meta.list
224
+ select (jsonb_populate_record(null::meta.list, '{"type": "static"}'::jsonb || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.list_id_seq'), 'app_id', v_app_id))).*
225
+ from jsonb_array_elements(coalesce(p_doc->'lists', '[]')) e;
226
+ for v_e in select * from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) loop
227
+ insert into meta.list_entry
228
+ select (jsonb_populate_record(null::meta.list_entry, '{"target_items": {}}'::jsonb || jsonb_strip_nulls(v_e) || jsonb_build_object(
229
+ 'id', nextval('meta.list_entry_id_seq'), 'app_id', v_app_id, 'parent_id', null))).*
230
+ returning id into v_new_id;
231
+ if v_e->>'id' is not null then
232
+ v_lmap := v_lmap || jsonb_build_object(v_e->>'id', v_new_id);
233
+ end if;
234
+ end loop;
235
+ update meta.list_entry x
236
+ set parent_id = (v_lmap->>(e->>'parent_id'))::int
237
+ from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) e
238
+ where e->>'parent_id' is not null and v_lmap ? (e->>'parent_id') and v_lmap ? (e->>'id')
239
+ and x.id = (v_lmap->>(e->>'id'))::int;
240
+ insert into meta.supporting_script
241
+ select (jsonb_populate_record(null::meta.supporting_script, '{"kind": "install", "seq": 10, "script": ""}'::jsonb || jsonb_strip_nulls(e)
242
+ || jsonb_build_object('id', nextval('meta.supporting_script_id_seq'), 'app_id', v_app_id))).*
243
+ from jsonb_array_elements(coalesce(p_doc->'supporting_scripts', '[]')) e;
244
+
245
+ -- (069) plug-ins
246
+ insert into meta.plugin
247
+ select (jsonb_populate_record(null::meta.plugin, e || jsonb_build_object('id', nextval('meta.plugin_id_seq'), 'app_id', v_app_id))).*
248
+ from jsonb_array_elements(coalesce(p_doc->'plugins', '[]')) e;
249
+
250
+ -- (080) search configurations (search regions name them)
251
+ insert into meta.search_config
252
+ select (jsonb_populate_record(null::meta.search_config, '{"seq": 10, "source_type": "table", "searchable_columns": [], "max_results": 10}'::jsonb
253
+ || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.search_config_id_seq'), 'app_id', v_app_id))).*
254
+ from jsonb_array_elements(coalesce(p_doc->'search_configs', '[]')) e;
255
+
256
+ -- (081) e-mail templates (meta.send_mail_template and the send_email process name them)
257
+ insert into meta.email_template
258
+ select (jsonb_populate_record(null::meta.email_template, e || jsonb_build_object('id', nextval('meta.email_template_id_seq'), 'app_id', v_app_id))).*
259
+ from jsonb_array_elements(coalesce(p_doc->'email_templates', '[]')) e;
260
+
261
+ -- (089) page groups
262
+ insert into meta.page_group
263
+ select (jsonb_populate_record(null::meta.page_group, e || jsonb_build_object('id', nextval('meta.page_group_id_seq'), 'app_id', v_app_id))).*
264
+ from jsonb_array_elements(coalesce(p_doc->'page_groups', '[]')) e;
265
+
266
+ -- (086) application settings and dynamic translations
267
+ insert into meta.app_setting
268
+ select (jsonb_populate_record(null::meta.app_setting, e || jsonb_build_object('id', nextval('meta.app_setting_id_seq'), 'app_id', v_app_id))).*
269
+ from jsonb_array_elements(coalesce(p_doc->'app_settings', '[]')) e;
270
+ insert into meta.dynamic_translation
271
+ select (jsonb_populate_record(null::meta.dynamic_translation, e || jsonb_build_object('id', nextval('meta.dynamic_translation_id_seq'), 'app_id', v_app_id))).*
272
+ from jsonb_array_elements(coalesce(p_doc->'dynamic_translations', '[]')) e;
273
+
274
+ -- (068) static application files
275
+ insert into meta.static_file (app_id, name, mime, content, updated_at)
276
+ select v_app_id, e->>'name', e->>'mime', decode(coalesce(e->>'content', ''), 'base64'), now()
277
+ from jsonb_array_elements(coalesce(p_doc->'static_files', '[]')) e;
278
+
279
+ for v_e in select * from jsonb_array_elements(coalesce(p_doc->'nav', '[]')) loop
280
+ insert into meta.nav_entry
281
+ select (jsonb_populate_record(null::meta.nav_entry, v_e || jsonb_build_object(
282
+ 'id', nextval('meta.nav_entry_id_seq'), 'app_id', v_app_id,
283
+ 'parent_id', v_nmap->>(v_e->>'parent_id')))).*
284
+ returning id into v_new_id;
285
+ v_nmap := v_nmap || jsonb_build_object(v_e->>'id', v_new_id);
286
+ end loop;
287
+
288
+ for v_page in select * from jsonb_array_elements(coalesce(p_doc->'pages', '[]')) loop
289
+ insert into meta.page
290
+ select (jsonb_populate_record(null::meta.page, v_page || jsonb_build_object('id', nextval('meta.page_id_seq'), 'app_id', v_app_id))).*
291
+ returning id into v_page_id;
292
+
293
+ v_rmap := '{}';
294
+ for v_e in select * from jsonb_array_elements(coalesce(v_page->'regions', '[]')) loop
295
+ insert into meta.region
296
+ select (jsonb_populate_record(null::meta.region, v_e || jsonb_build_object('id', nextval('meta.region_id_seq'), 'page_id', v_page_id))).*
297
+ returning id into v_new_id;
298
+ v_rmap := v_rmap || jsonb_build_object(v_e->>'id', v_new_id);
299
+ end loop;
300
+ -- facet and map regions point at their report region by id
301
+ update meta.region r
302
+ set config = jsonb_set(r.config, '{report}', to_jsonb((v_rmap->>(r.config->>'report'))::int))
303
+ where r.page_id = v_page_id and r.type in ('facets', 'smart_filters', 'map') and v_rmap ? (r.config->>'report');
304
+
305
+ insert into meta.item
306
+ select (jsonb_populate_record(null::meta.item, e || jsonb_build_object('id', nextval('meta.item_id_seq'), 'page_id', v_page_id, 'region_id', v_rmap->>(e->>'region_id')))).*
307
+ from jsonb_array_elements(coalesce(v_page->'items', '[]')) e;
308
+ insert into meta.button
309
+ select (jsonb_populate_record(null::meta.button, e || jsonb_build_object('id', nextval('meta.button_id_seq'), 'page_id', v_page_id, 'region_id', v_rmap->>(e->>'region_id')))).*
310
+ from jsonb_array_elements(coalesce(v_page->'buttons', '[]')) e;
311
+ insert into meta.dynamic_action
312
+ select (jsonb_populate_record(null::meta.dynamic_action, e || jsonb_build_object('id', nextval('meta.dynamic_action_id_seq'), 'page_id', v_page_id, 'affected_region_id', v_rmap->>(e->>'affected_region_id')))).*
313
+ from jsonb_array_elements(coalesce(v_page->'dynamic_actions', '[]')) e;
314
+ insert into meta.validation
315
+ select (jsonb_populate_record(null::meta.validation, e || jsonb_build_object('id', nextval('meta.validation_id_seq'), 'page_id', v_page_id))).*
316
+ from jsonb_array_elements(coalesce(v_page->'validations', '[]')) e;
317
+ insert into meta.process
318
+ select (jsonb_populate_record(null::meta.process, e || jsonb_build_object('id', nextval('meta.process_id_seq'), 'page_id', v_page_id, 'region_id', v_rmap->>(e->>'region_id')))).*
319
+ from jsonb_array_elements(coalesce(v_page->'processes', '[]')) e;
320
+ insert into meta.computation
321
+ select (jsonb_populate_record(null::meta.computation, e || jsonb_build_object('id', nextval('meta.computation_id_seq'), 'page_id', v_page_id))).*
322
+ from jsonb_array_elements(coalesce(v_page->'computations', '[]')) e;
323
+ insert into meta.branch
324
+ select (jsonb_populate_record(null::meta.branch, e || jsonb_build_object('id', nextval('meta.branch_id_seq'), 'page_id', v_page_id))).*
325
+ from jsonb_array_elements(coalesce(v_page->'branches', '[]')) e;
326
+ end loop;
327
+
328
+ return v_app_id;
329
+ end
330
+ $$;