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,621 @@
1
+ -- =====================================================================
2
+ -- 081: sending e-mail (APEX: APEX_MAIL, Email Templates, the "Send E-Mail"
3
+ -- process), through SMTP (password or OAuth2 / XOAUTH2), Microsoft Graph,
4
+ -- Amazon SES or an HTTP API (Resend, Postmark, SendGrid, Mailgun)
5
+ --
6
+ -- This replaces the decision of 008 (no e-mail): the project owner asked
7
+ -- for it on 2026-10-12.
8
+ --
9
+ -- meta.mail_service: an instance-level mail service (administrators,
10
+ -- Workspace utilities → Mail services). Provider, settings without
11
+ -- secrets, the secret (password, client secret or API key) encrypted by
12
+ -- the server with PGKILN_SECRET_KEY (write-only, never exported, never
13
+ -- readable by applications), the From addresses it may send as.
14
+ -- meta.app_mail_service: the service an application sends through, its
15
+ -- From address and a daily limit. Installation data: not exported.
16
+ -- meta.mail_queue, meta.mail_attachment: the outbox. meta.send_mail(),
17
+ -- meta.send_mail_template() and meta.add_mail_attachment() write it in
18
+ -- the caller's transaction (a rollback sends nothing) and NOTIFY
19
+ -- pgkiln_mail; the server sends after the commit (src/mail/), retries
20
+ -- with growing pauses, and keeps the log 30 days.
21
+ -- meta.email_template: an application's e-mail templates (subject, HTML
22
+ -- and plain text with #PLACEHOLDERS#), exported with the application.
23
+ --
24
+ -- The functions run for the current application (meta.app_id()) only when
25
+ -- the caller runs as that application's database role, so application code
26
+ -- can't send in another application's name.
27
+ --
28
+ -- export_app / import_app are redefined from 080 to carry email_templates.
29
+ -- =====================================================================
30
+
31
+ create table meta.mail_service (
32
+ id serial primary key,
33
+ name text not null unique check (name ~ '^[A-Z][A-Z0-9_]{0,59}$'),
34
+ description text,
35
+ provider text not null check (provider in ('smtp', 'graph', 'ses', 'resend', 'postmark', 'sendgrid', 'mailgun', 'log')),
36
+ -- host, port, security, auth, username, token_url, client_id, scope, tenant_id, access_key_id,
37
+ -- region, domain, message_stream, save_to_sent_items, base_url (administrators only); no secrets
38
+ settings jsonb not null default '{}' check (jsonb_typeof(settings) = 'object'),
39
+ -- "v1:" + base64(iv, tag, ciphertext); written by the server only (src/secrets.ts)
40
+ secret_enc text,
41
+ -- the From addresses it may send as: user@example.com, or @example.com for a whole domain
42
+ senders text[] not null default '{}',
43
+ enabled boolean not null default true,
44
+ created_at timestamptz not null default now(),
45
+ updated_at timestamptz not null default now()
46
+ );
47
+ revoke all on meta.mail_service from public;
48
+ comment on table meta.mail_service is 'Mail services of this installation (081); the secret is encrypted and never readable by applications';
49
+
50
+ create table meta.app_mail_service (
51
+ app_id int primary key references meta.app on delete cascade,
52
+ service_id int not null references meta.mail_service on delete cascade,
53
+ -- the application's From address when the caller gives none
54
+ from_address text,
55
+ -- per day (UTC); null = no limit
56
+ max_per_day int check (max_per_day >= 0)
57
+ );
58
+ revoke all on meta.app_mail_service from public;
59
+
60
+ create table meta.mail_queue (
61
+ id bigint generated always as identity primary key,
62
+ app_id int not null references meta.app on delete cascade,
63
+ username text,
64
+ from_address text,
65
+ reply_to text,
66
+ to_addresses text[] not null default '{}',
67
+ cc_addresses text[] not null default '{}',
68
+ bcc_addresses text[] not null default '{}',
69
+ subject text not null,
70
+ body_text text,
71
+ body_html text,
72
+ template text,
73
+ status text not null default 'queued' check (status in ('queued', 'sending', 'sent', 'error', 'cancelled')),
74
+ attempts int not null default 0,
75
+ not_before timestamptz not null default now(),
76
+ -- the last problem, or what the provider said
77
+ message text,
78
+ -- the provider's id of the message, the service that sent it
79
+ provider_id text,
80
+ service text,
81
+ created_at timestamptz not null default now(),
82
+ -- attachments may be added in the transaction that queued the mail only
83
+ created_xact bigint not null default txid_current(),
84
+ sent_at timestamptz,
85
+ check (cardinality(to_addresses) + cardinality(cc_addresses) + cardinality(bcc_addresses) between 1 and 50)
86
+ );
87
+ create index mail_queue_queued on meta.mail_queue (not_before) where status = 'queued';
88
+ create index mail_queue_app on meta.mail_queue (app_id, created_at);
89
+ revoke all on meta.mail_queue from public;
90
+
91
+ create table meta.mail_attachment (
92
+ id bigint generated always as identity primary key,
93
+ mail_id bigint not null references meta.mail_queue on delete cascade,
94
+ filename text not null check (filename ~ '^[^/\\\r\n"<>:|?*]{1,200}$'),
95
+ mime text not null check (mime ~ '^[a-z]+/[a-z0-9.+-]{1,100}$'),
96
+ content bytea not null check (octet_length(content) <= 10 * 1024 * 1024)
97
+ );
98
+ create index mail_attachment_mail on meta.mail_attachment (mail_id);
99
+ revoke all on meta.mail_attachment from public;
100
+
101
+ create table meta.email_template (
102
+ id serial primary key,
103
+ app_id int not null references meta.app on delete cascade,
104
+ name text not null check (name ~ '^[A-Z][A-Z0-9_]{0,59}$'),
105
+ subject text not null check (length(subject) between 1 and 998 and subject !~ '[\r\n]'),
106
+ html_body text,
107
+ text_body text,
108
+ unique (app_id, name)
109
+ );
110
+ revoke all on meta.email_template from public;
111
+ comment on table meta.email_template is 'E-mail templates (081): subject, HTML and plain text with #PLACEHOLDERS#; exported with the application';
112
+
113
+ alter table meta.process drop constraint process_type_check;
114
+ alter table meta.process add constraint process_type_check check (type in (
115
+ 'ai_generate', 'chain', 'data_load', 'download', 'form_dml', 'grid_dml', 'invoke_api', 'sql', 'workflow', 'plugin', 'send_push', 'send_email'));
116
+
117
+ -- ---------------------------------------------------------------- SQL API
118
+
119
+ -- The addresses of a list (comma or semicolon separated, or one per element),
120
+ -- checked: plain addresses only (no names, no line breaks), each at most 254
121
+ -- characters. Raises with p_what in the message.
122
+ create function meta.mail_addresses(p_list text, p_what text) returns text[]
123
+ language plpgsql immutable set search_path = pg_catalog as $$
124
+ declare
125
+ v_out text[] := '{}';
126
+ v text;
127
+ begin
128
+ if p_list is null or btrim(p_list) = '' then
129
+ return v_out;
130
+ end if;
131
+ foreach v in array regexp_split_to_array(p_list, '\s*[,;]\s*') loop
132
+ v := btrim(v);
133
+ continue when v = '';
134
+ if length(v) > 254 or v !~ '^[A-Za-z0-9.!#$%&''*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$' then
135
+ raise exception 'meta.send_mail: "%" is not an e-mail address (%)', left(v, 100), p_what;
136
+ end if;
137
+ if not v = any (v_out) then
138
+ v_out := v_out || v;
139
+ end if;
140
+ end loop;
141
+ return v_out;
142
+ end
143
+ $$;
144
+
145
+ -- The calling application and its mail service: the current application, called as its own
146
+ -- database role, with a mail service an administrator gave it.
147
+ create function meta.mail_caller(out app_id int, out service_id int, out from_address text, out max_per_day int, out senders text[])
148
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
149
+ declare
150
+ v_role text;
151
+ begin
152
+ app_id := meta.app_id();
153
+ if app_id is null then
154
+ raise exception 'meta.send_mail: no current application (call it from application code)';
155
+ end if;
156
+ select a.db_role into v_role from meta.app a where a.id = app_id;
157
+ if not found then
158
+ raise exception 'meta.send_mail: no current application (call it from application code)';
159
+ end if;
160
+ if v_role is not null and current_setting('role') is distinct from v_role then
161
+ raise exception 'meta.send_mail: called outside the application''s own database role';
162
+ end if;
163
+ select x.service_id, coalesce(x.from_address, nullif(s.settings->>'default_from', '')), x.max_per_day, s.senders
164
+ into service_id, from_address, max_per_day, senders
165
+ from meta.app_mail_service x join meta.mail_service s on s.id = x.service_id
166
+ where x.app_id = mail_caller.app_id and s.enabled;
167
+ if not found then
168
+ raise exception 'meta.send_mail: this application may not send e-mail (an administrator chooses its mail service under Workspace utilities → Mail services)';
169
+ end if;
170
+ end
171
+ $$;
172
+ revoke all on function meta.mail_caller() from public;
173
+
174
+ -- May the service send as this address? (user@example.com, or @example.com for the whole domain)
175
+ create function meta.mail_sender_allowed(p_senders text[], p_from text) returns boolean
176
+ language sql immutable set search_path = pg_catalog as $$
177
+ select exists (select 1 from unnest(p_senders) s
178
+ where lower(s) = lower(p_from) or (left(s, 1) = '@' and lower(p_from) like '%' || lower(s)))
179
+ $$;
180
+
181
+ -- Queue one mail (the functions below check their arguments first).
182
+ create function meta.mail_enqueue(p_to text, p_subject text, p_body text, p_body_html text, p_from text, p_cc text, p_bcc text, p_reply_to text, p_template text)
183
+ returns bigint
184
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
185
+ declare
186
+ c record;
187
+ v_to text[] := meta.mail_addresses(p_to, 'to');
188
+ v_cc text[] := meta.mail_addresses(p_cc, 'cc');
189
+ v_bcc text[] := meta.mail_addresses(p_bcc, 'bcc');
190
+ v_reply text[] := meta.mail_addresses(p_reply_to, 'reply to');
191
+ v_from text[];
192
+ v_count int;
193
+ v_id bigint;
194
+ begin
195
+ select * into c from meta.mail_caller();
196
+ if cardinality(v_to) + cardinality(v_cc) + cardinality(v_bcc) = 0 then
197
+ raise exception 'meta.send_mail: no recipient';
198
+ end if;
199
+ if cardinality(v_to) + cardinality(v_cc) + cardinality(v_bcc) > 50 then
200
+ raise exception 'meta.send_mail: at most 50 recipients per mail (to, cc and bcc together)';
201
+ end if;
202
+ if cardinality(v_reply) > 1 then
203
+ raise exception 'meta.send_mail: one reply-to address';
204
+ end if;
205
+ if p_subject is null or length(p_subject) not between 1 and 998 or p_subject ~ '[\r\n]' then
206
+ raise exception 'meta.send_mail: the subject is required, on one line (at most 998 characters)';
207
+ end if;
208
+ if p_body is null and p_body_html is null then
209
+ raise exception 'meta.send_mail: a plain text or HTML body is required';
210
+ end if;
211
+ if coalesce(octet_length(p_body), 0) > 1048576 or coalesce(octet_length(p_body_html), 0) > 1048576 then
212
+ raise exception 'meta.send_mail: each body is at most 1 MB';
213
+ end if;
214
+ v_from := meta.mail_addresses(coalesce(nullif(btrim(p_from), ''), c.from_address), 'from');
215
+ if cardinality(v_from) <> 1 then
216
+ raise exception 'meta.send_mail: no From address (give one, or an administrator sets the application''s)';
217
+ end if;
218
+ if not meta.mail_sender_allowed(c.senders, v_from[1]) then
219
+ raise exception 'meta.send_mail: the mail service may not send as % (its allowed senders are set by an administrator)', v_from[1];
220
+ end if;
221
+ if c.max_per_day is not null then
222
+ select count(*) into v_count from meta.mail_queue
223
+ where app_id = c.app_id and created_at >= date_trunc('day', now() at time zone 'UTC') at time zone 'UTC' and status <> 'cancelled';
224
+ if v_count >= c.max_per_day then
225
+ raise exception 'meta.send_mail: this application''s limit of % mails per day is reached', c.max_per_day;
226
+ end if;
227
+ end if;
228
+ insert into meta.mail_queue (app_id, username, from_address, reply_to, to_addresses, cc_addresses, bcc_addresses, subject, body_text, body_html, template)
229
+ values (c.app_id, nullif(current_setting('pgkiln.app_user', true), ''), v_from[1], v_reply[1], v_to, v_cc, v_bcc, p_subject, p_body, p_body_html, p_template)
230
+ returning id into v_id;
231
+ perform pg_notify('pgkiln_mail', v_id::text);
232
+ return v_id;
233
+ end
234
+ $$;
235
+ revoke all on function meta.mail_enqueue(text, text, text, text, text, text, text, text, text) from public;
236
+
237
+ -- APEX_MAIL.SEND: queue a mail; it is sent after the transaction commits.
238
+ create function meta.send_mail(
239
+ p_to text,
240
+ p_subject text,
241
+ p_body text default null,
242
+ p_body_html text default null,
243
+ p_from text default null,
244
+ p_cc text default null,
245
+ p_bcc text default null,
246
+ p_reply_to text default null
247
+ ) returns bigint
248
+ language sql volatile security definer set search_path = meta, pg_catalog as $$
249
+ select meta.mail_enqueue(p_to, p_subject, p_body, p_body_html, p_from, p_cc, p_bcc, p_reply_to, null)
250
+ $$;
251
+ grant execute on function meta.send_mail(text, text, text, text, text, text, text, text) to public;
252
+
253
+ -- A template with its placeholders filled, in one pass (a value is never searched for
254
+ -- placeholders itself): #NAME# is the value (HTML-escaped in the HTML body), #NAME!RAW#
255
+ -- the value as it is (trusted HTML). Names are case-insensitive; unknown ones stay.
256
+ create function meta.mail_fill(p_text text, p_values jsonb, p_html boolean) returns text
257
+ language plpgsql immutable set search_path = meta, pg_catalog as $$
258
+ declare
259
+ c_pat constant text := '#([A-Za-z][A-Za-z0-9_]{0,59})(!RAW)?#';
260
+ v_vals jsonb;
261
+ v_out text := '';
262
+ v_rest text := p_text;
263
+ v_pos int;
264
+ v_tok text;
265
+ v_m text[];
266
+ v_val text;
267
+ begin
268
+ if p_text is null then
269
+ return null;
270
+ end if;
271
+ select coalesce(jsonb_object_agg(upper(key), value), '{}') into v_vals
272
+ from jsonb_each(case when jsonb_typeof(p_values) = 'object' then p_values else '{}' end);
273
+ loop
274
+ v_pos := regexp_instr(v_rest, c_pat);
275
+ exit when v_pos = 0;
276
+ v_tok := regexp_substr(v_rest, c_pat);
277
+ v_m := regexp_match(v_tok, '^' || c_pat || '$');
278
+ v_out := v_out || left(v_rest, v_pos - 1);
279
+ if v_vals ? upper(v_m[1]) then
280
+ v_val := case jsonb_typeof(v_vals->upper(v_m[1])) when 'string' then v_vals->>upper(v_m[1]) when 'null' then '' else (v_vals->upper(v_m[1]))::text end;
281
+ v_out := v_out || case when p_html and v_m[2] is null then meta.html_escape(v_val) else v_val end;
282
+ else
283
+ v_out := v_out || v_tok;
284
+ end if;
285
+ v_rest := substr(v_rest, v_pos + length(v_tok));
286
+ end loop;
287
+ return v_out || v_rest;
288
+ end
289
+ $$;
290
+
291
+ -- APEX_MAIL.SEND with p_template_static_id and p_placeholders.
292
+ create function meta.send_mail_template(
293
+ p_template text,
294
+ p_placeholders jsonb,
295
+ p_to text,
296
+ p_cc text default null,
297
+ p_bcc text default null,
298
+ p_from text default null,
299
+ p_reply_to text default null
300
+ ) returns bigint
301
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
302
+ declare
303
+ t record;
304
+ begin
305
+ select * into t from meta.email_template where app_id = meta.app_id() and name = upper(p_template);
306
+ if not found then
307
+ raise exception 'meta.send_mail_template: no e-mail template %', p_template;
308
+ end if;
309
+ return meta.mail_enqueue(p_to, regexp_replace(meta.mail_fill(t.subject, p_placeholders, false), '[\r\n]+', ' ', 'g'),
310
+ meta.mail_fill(t.text_body, p_placeholders, false), meta.mail_fill(t.html_body, p_placeholders, true),
311
+ p_from, p_cc, p_bcc, p_reply_to, t.name);
312
+ end
313
+ $$;
314
+ grant execute on function meta.send_mail_template(text, jsonb, text, text, text, text, text) to public;
315
+
316
+ -- APEX_MAIL.ADD_ATTACHMENT: a file for a mail queued in this transaction.
317
+ create function meta.add_mail_attachment(p_mail_id bigint, p_content bytea, p_filename text, p_mime text default 'application/octet-stream')
318
+ returns void
319
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
320
+ declare
321
+ c record;
322
+ v_count int;
323
+ v_size bigint;
324
+ begin
325
+ select * into c from meta.mail_caller();
326
+ if not exists (select 1 from meta.mail_queue where id = p_mail_id and app_id = c.app_id and status = 'queued' and created_xact = txid_current()) then
327
+ raise exception 'meta.add_mail_attachment: mail % was not queued in this transaction', p_mail_id;
328
+ end if;
329
+ if p_content is null then
330
+ raise exception 'meta.add_mail_attachment: no content';
331
+ end if;
332
+ select count(*), coalesce(sum(octet_length(content)), 0) into v_count, v_size from meta.mail_attachment where mail_id = p_mail_id;
333
+ if v_count >= 20 or v_size + octet_length(p_content) > 20 * 1024 * 1024 then
334
+ raise exception 'meta.add_mail_attachment: at most 20 attachments and 20 MB per mail';
335
+ end if;
336
+ insert into meta.mail_attachment (mail_id, filename, mime, content)
337
+ values (p_mail_id, regexp_replace(coalesce(p_filename, 'attachment'), '[/\\\r\n"<>:|?*]', '_', 'g'), lower(coalesce(p_mime, 'application/octet-stream')), p_content);
338
+ end
339
+ $$;
340
+ grant execute on function meta.add_mail_attachment(bigint, bytea, text, text) to public;
341
+
342
+ -- The status of a mail of the current application: queued, sending, sent, error or cancelled.
343
+ create function meta.mail_status(p_mail_id bigint) returns text
344
+ language sql stable security definer set search_path = meta, pg_catalog as $$
345
+ select status from meta.mail_queue where id = p_mail_id and app_id = meta.app_id()
346
+ $$;
347
+ grant execute on function meta.mail_status(bigint) to public;
348
+
349
+ -- ---------------------------------------------------------------- export / import
350
+ -- Same as 080, plus email_templates. Mail services, the application's service, the queue
351
+ -- and attachments belong to the installation: not exported.
352
+ create or replace function meta.export_app(p_alias text) returns jsonb
353
+ language sql stable set search_path = meta, pg_catalog as $$
354
+ select jsonb_build_object(
355
+ 'format', 'pgkiln/2',
356
+ 'app', to_jsonb(a) - 'id' - 'created_at' - 'updated_at',
357
+ '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), '[]'),
358
+ '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), '[]'),
359
+ '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), '[]'),
360
+ '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), '[]'),
361
+ '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), '[]'),
362
+ '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), '[]'),
363
+ '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), '[]'),
364
+ '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)
365
+ from meta.report_layout x where x.app_id = a.id), '[]'),
366
+ 'automations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'next_run_at' - 'last_run_at' - 'last_status' - 'code' order by x.name)
367
+ from meta.automation x where x.app_id = a.id), '[]'),
368
+ -- (044) the actions of the automations, by automation name
369
+ '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")
370
+ from meta.automation_action x where x.app_id = a.id), '[]'),
371
+ '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), '[]'),
372
+ '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), '[]'),
373
+ '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), '[]'),
374
+ '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), '[]'),
375
+ '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), '[]'),
376
+ '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), '[]'),
377
+ -- (030) secrets never leave the installation: they are entered again after an import
378
+ -- (050) nor do the password and refresh token of the OAuth2 flows, or the token state
379
+ '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)
380
+ from meta.web_credential x where x.app_id = a.id), '[]'),
381
+ -- (050) the synchronisation's state (next and last run) belongs to the installation
382
+ '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)
383
+ from meta.rest_source x where x.app_id = a.id), '[]'),
384
+ -- (041) data load definitions
385
+ '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), '[]'),
386
+ -- (042) lists; entries keep their ids so parents can be remapped on import
387
+ '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), '[]'),
388
+ '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), '[]'),
389
+ -- (042) supporting objects: install, upgrade and deinstall scripts (never run on import)
390
+ '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), '[]'),
391
+ -- (069) plug-ins (their files are static files, their templates template components)
392
+ '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), '[]'),
393
+ -- (080) search configurations
394
+ '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), '[]'),
395
+ -- (081) e-mail templates
396
+ '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), '[]'),
397
+ -- (068) static application files (JavaScript, CSS, images, fonts) as base64
398
+ '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)
399
+ from meta.static_file x where x.app_id = a.id), '[]'),
400
+ -- nav entries and regions keep their ids, so parents and references can be remapped on import
401
+ '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), '[]'),
402
+ 'pages', coalesce((
403
+ select jsonb_agg(to_jsonb(p) - 'id' - 'app_id' || jsonb_build_object(
404
+ '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), '[]'),
405
+ '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), '[]'),
406
+ '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), '[]'),
407
+ '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), '[]'),
408
+ '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), '[]'),
409
+ '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), '[]'),
410
+ '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), '[]'),
411
+ '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), '[]')
412
+ ) order by p.page_no)
413
+ from meta.page p where p.app_id = a.id), '[]'))
414
+ from meta.app a
415
+ where a.alias = p_alias
416
+ $$;
417
+
418
+ create or replace function meta.import_app(p_doc jsonb, p_alias text default null) returns int
419
+ language plpgsql set search_path = meta, pg_catalog as $$
420
+ declare
421
+ v_app_id int;
422
+ v_page_id int;
423
+ v_page jsonb;
424
+ v_e jsonb;
425
+ v_rmap jsonb;
426
+ v_nmap jsonb := '{}';
427
+ v_lmap jsonb := '{}';
428
+ v_new_id int;
429
+ begin
430
+ if p_doc->>'format' is distinct from 'pgkiln/2' then
431
+ raise exception 'unsupported export format %', coalesce(p_doc->>'format', '(none)');
432
+ end if;
433
+
434
+ insert into meta.app
435
+ select (jsonb_populate_record(null::meta.app, p_doc->'app' || jsonb_build_object(
436
+ 'id', nextval('meta.app_id_seq'),
437
+ 'alias', coalesce(p_alias, p_doc->'app'->>'alias'),
438
+ 'created_at', now(), 'updated_at', now()))).*
439
+ returning id into v_app_id;
440
+
441
+ insert into meta.authz_scheme
442
+ select (jsonb_populate_record(null::meta.authz_scheme, e || jsonb_build_object('id', nextval('meta.authz_scheme_id_seq'), 'app_id', v_app_id))).*
443
+ from jsonb_array_elements(coalesce(p_doc->'authz_schemes', '[]')) e;
444
+ insert into meta.app_item
445
+ select (jsonb_populate_record(null::meta.app_item, e || jsonb_build_object('id', nextval('meta.app_item_id_seq'), 'app_id', v_app_id))).*
446
+ from jsonb_array_elements(coalesce(p_doc->'app_items', '[]')) e;
447
+ insert into meta.app_process
448
+ select (jsonb_populate_record(null::meta.app_process, e || jsonb_build_object('id', nextval('meta.app_process_id_seq'), 'app_id', v_app_id))).*
449
+ from jsonb_array_elements(coalesce(p_doc->'app_processes', '[]')) e;
450
+ insert into meta.lov
451
+ select (jsonb_populate_record(null::meta.lov, e || jsonb_build_object('id', nextval('meta.lov_id_seq'), 'app_id', v_app_id))).*
452
+ from jsonb_array_elements(coalesce(p_doc->'lovs', '[]')) e;
453
+ insert into meta.app_group_role
454
+ select (jsonb_populate_record(null::meta.app_group_role, e || jsonb_build_object('app_id', v_app_id))).*
455
+ from jsonb_array_elements(coalesce(p_doc->'group_roles', '[]')) e;
456
+ insert into meta.text_message
457
+ select (jsonb_populate_record(null::meta.text_message, e || jsonb_build_object('app_id', v_app_id))).*
458
+ from jsonb_array_elements(coalesce(p_doc->'text_messages', '[]')) e;
459
+ insert into meta.translation
460
+ select (jsonb_populate_record(null::meta.translation, e || jsonb_build_object('app_id', v_app_id))).*
461
+ from jsonb_array_elements(coalesce(p_doc->'translations', '[]')) e;
462
+ insert into meta.report_layout
463
+ select (jsonb_populate_record(null::meta.report_layout, (e - 'logo') || jsonb_build_object(
464
+ 'id', nextval('meta.report_layout_id_seq'), 'app_id', v_app_id,
465
+ 'logo', null))).*
466
+ from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e;
467
+ update meta.report_layout l
468
+ set logo = decode(e->>'logo', 'base64')
469
+ from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e
470
+ where l.app_id = v_app_id and l.name = e->>'name' and e->>'logo' is not null;
471
+ -- imported automations start switched off: the copy must not run the original's jobs unasked.
472
+ -- (044) a file with actions: the automations' code is ignored; an older file: code becomes the single action
473
+ insert into meta.automation
474
+ select (jsonb_populate_record(null::meta.automation, '{"error_handling": "stop"}'::jsonb
475
+ || jsonb_strip_nulls(case when p_doc ? 'automation_actions' then e - 'code' else e end)
476
+ || jsonb_build_object('id', nextval('meta.automation_id_seq'), 'app_id', v_app_id, 'enabled', false))).*
477
+ from jsonb_array_elements(coalesce(p_doc->'automations', '[]')) e;
478
+ insert into meta.automation_action
479
+ select (jsonb_populate_record(null::meta.automation_action, '{"seq": 10}'::jsonb || jsonb_strip_nulls(e)
480
+ || jsonb_build_object('id', nextval('meta.automation_action_id_seq'), 'app_id', v_app_id))).*
481
+ from jsonb_array_elements(coalesce(p_doc->'automation_actions', '[]')) e;
482
+ insert into meta.document_template
483
+ select (jsonb_populate_record(null::meta.document_template, e || jsonb_build_object('id', nextval('meta.document_template_id_seq'), 'app_id', v_app_id))).*
484
+ from jsonb_array_elements(coalesce(p_doc->'document_templates', '[]')) e;
485
+ insert into meta.task_definition
486
+ select (jsonb_populate_record(null::meta.task_definition, e || jsonb_build_object('id', nextval('meta.task_definition_id_seq'), 'app_id', v_app_id))).*
487
+ from jsonb_array_elements(coalesce(p_doc->'task_definitions', '[]')) e;
488
+ insert into meta.workflow_definition
489
+ select (jsonb_populate_record(null::meta.workflow_definition, e || jsonb_build_object('id', nextval('meta.workflow_definition_id_seq'), 'app_id', v_app_id))).*
490
+ from jsonb_array_elements(coalesce(p_doc->'workflow_definitions', '[]')) e;
491
+ insert into meta.rest_module
492
+ select (jsonb_populate_record(null::meta.rest_module, e || jsonb_build_object('id', nextval('meta.rest_module_id_seq'), 'app_id', v_app_id))).*
493
+ from jsonb_array_elements(coalesce(p_doc->'rest_modules', '[]')) e;
494
+ -- (028) template components: regions and report columns refer to them by static id
495
+ insert into meta.template_component
496
+ select (jsonb_populate_record(null::meta.template_component, e || jsonb_build_object('id', nextval('meta.template_component_id_seq'), 'app_id', v_app_id))).*
497
+ from jsonb_array_elements(coalesce(p_doc->'template_components', '[]')) e;
498
+ -- (029) build options (components name them by name)
499
+ insert into meta.build_option
500
+ select (jsonb_populate_record(null::meta.build_option, e || jsonb_build_object('id', nextval('meta.build_option_id_seq'), 'app_id', v_app_id))).*
501
+ from jsonb_array_elements(coalesce(p_doc->'build_options', '[]')) e;
502
+ -- (030) web credentials arrive without a secret, whatever the document holds
503
+ -- (050) nor a password or refresh token
504
+ insert into meta.web_credential
505
+ select (jsonb_populate_record(null::meta.web_credential, '{"type": "basic", "valid_for": [], "grant_type": "client_credentials"}'::jsonb
506
+ || jsonb_strip_nulls(e - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at')
507
+ || jsonb_build_object('id', nextval('meta.web_credential_id_seq'), 'app_id', v_app_id))).*
508
+ from jsonb_array_elements(coalesce(p_doc->'web_credentials', '[]')) e;
509
+ -- (050) write-back operations and synchronisation; an imported synchronisation starts switched off
510
+ insert into meta.rest_source
511
+ select (jsonb_populate_record(null::meta.rest_source, '{"method": "GET", "headers": {}, "params": [], "columns": [], "cache_seconds": 0, "timeout_s": 10, "max_rows": 1000,
512
+ "operations": {}, "key_columns": [], "sync_mode": "merge", "sync_delete": false, "sync_time_zone": "UTC"}'::jsonb
513
+ || jsonb_strip_nulls(e - 'sync_next_at' - 'sync_last_at' - 'sync_last_status')
514
+ || jsonb_build_object('id', nextval('meta.rest_source_id_seq'), 'app_id', v_app_id, 'sync_enabled', false))).*
515
+ from jsonb_array_elements(coalesce(p_doc->'rest_sources', '[]')) e;
516
+
517
+ -- (041) data load definitions (the data_load process names them)
518
+ insert into meta.data_load_def
519
+ select (jsonb_populate_record(null::meta.data_load_def, '{"format": "auto", "headers": true, "mode": "append", "skip_errors": false, "columns": []}'::jsonb
520
+ || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.data_load_def_id_seq'), 'app_id', v_app_id))).*
521
+ from jsonb_array_elements(coalesce(p_doc->'data_load_definitions', '[]')) e;
522
+
523
+ -- (042) lists and their entries, supporting scripts. Entries are inserted
524
+ -- without a parent first and the parents set afterwards, so the order of
525
+ -- the entries in the document doesn't matter (a moved entry may have a
526
+ -- higher id than its children).
527
+ insert into meta.list
528
+ 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))).*
529
+ from jsonb_array_elements(coalesce(p_doc->'lists', '[]')) e;
530
+ for v_e in select * from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) loop
531
+ insert into meta.list_entry
532
+ select (jsonb_populate_record(null::meta.list_entry, '{"target_items": {}}'::jsonb || jsonb_strip_nulls(v_e) || jsonb_build_object(
533
+ 'id', nextval('meta.list_entry_id_seq'), 'app_id', v_app_id, 'parent_id', null))).*
534
+ returning id into v_new_id;
535
+ if v_e->>'id' is not null then
536
+ v_lmap := v_lmap || jsonb_build_object(v_e->>'id', v_new_id);
537
+ end if;
538
+ end loop;
539
+ update meta.list_entry x
540
+ set parent_id = (v_lmap->>(e->>'parent_id'))::int
541
+ from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) e
542
+ where e->>'parent_id' is not null and v_lmap ? (e->>'parent_id') and v_lmap ? (e->>'id')
543
+ and x.id = (v_lmap->>(e->>'id'))::int;
544
+ insert into meta.supporting_script
545
+ select (jsonb_populate_record(null::meta.supporting_script, '{"kind": "install", "seq": 10, "script": ""}'::jsonb || jsonb_strip_nulls(e)
546
+ || jsonb_build_object('id', nextval('meta.supporting_script_id_seq'), 'app_id', v_app_id))).*
547
+ from jsonb_array_elements(coalesce(p_doc->'supporting_scripts', '[]')) e;
548
+
549
+ -- (069) plug-ins
550
+ insert into meta.plugin
551
+ select (jsonb_populate_record(null::meta.plugin, e || jsonb_build_object('id', nextval('meta.plugin_id_seq'), 'app_id', v_app_id))).*
552
+ from jsonb_array_elements(coalesce(p_doc->'plugins', '[]')) e;
553
+
554
+ -- (080) search configurations (search regions name them)
555
+ insert into meta.search_config
556
+ select (jsonb_populate_record(null::meta.search_config, '{"seq": 10, "source_type": "table", "searchable_columns": [], "max_results": 10}'::jsonb
557
+ || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.search_config_id_seq'), 'app_id', v_app_id))).*
558
+ from jsonb_array_elements(coalesce(p_doc->'search_configs', '[]')) e;
559
+
560
+ -- (081) e-mail templates (meta.send_mail_template and the send_email process name them)
561
+ insert into meta.email_template
562
+ select (jsonb_populate_record(null::meta.email_template, e || jsonb_build_object('id', nextval('meta.email_template_id_seq'), 'app_id', v_app_id))).*
563
+ from jsonb_array_elements(coalesce(p_doc->'email_templates', '[]')) e;
564
+
565
+ -- (068) static application files
566
+ insert into meta.static_file (app_id, name, mime, content, updated_at)
567
+ select v_app_id, e->>'name', e->>'mime', decode(coalesce(e->>'content', ''), 'base64'), now()
568
+ from jsonb_array_elements(coalesce(p_doc->'static_files', '[]')) e;
569
+
570
+ for v_e in select * from jsonb_array_elements(coalesce(p_doc->'nav', '[]')) loop
571
+ insert into meta.nav_entry
572
+ select (jsonb_populate_record(null::meta.nav_entry, v_e || jsonb_build_object(
573
+ 'id', nextval('meta.nav_entry_id_seq'), 'app_id', v_app_id,
574
+ 'parent_id', v_nmap->>(v_e->>'parent_id')))).*
575
+ returning id into v_new_id;
576
+ v_nmap := v_nmap || jsonb_build_object(v_e->>'id', v_new_id);
577
+ end loop;
578
+
579
+ for v_page in select * from jsonb_array_elements(coalesce(p_doc->'pages', '[]')) loop
580
+ insert into meta.page
581
+ select (jsonb_populate_record(null::meta.page, v_page || jsonb_build_object('id', nextval('meta.page_id_seq'), 'app_id', v_app_id))).*
582
+ returning id into v_page_id;
583
+
584
+ v_rmap := '{}';
585
+ for v_e in select * from jsonb_array_elements(coalesce(v_page->'regions', '[]')) loop
586
+ insert into meta.region
587
+ select (jsonb_populate_record(null::meta.region, v_e || jsonb_build_object('id', nextval('meta.region_id_seq'), 'page_id', v_page_id))).*
588
+ returning id into v_new_id;
589
+ v_rmap := v_rmap || jsonb_build_object(v_e->>'id', v_new_id);
590
+ end loop;
591
+ -- facet and map regions point at their report region by id
592
+ update meta.region r
593
+ set config = jsonb_set(r.config, '{report}', to_jsonb((v_rmap->>(r.config->>'report'))::int))
594
+ where r.page_id = v_page_id and r.type in ('facets', 'smart_filters', 'map') and v_rmap ? (r.config->>'report');
595
+
596
+ insert into meta.item
597
+ 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')))).*
598
+ from jsonb_array_elements(coalesce(v_page->'items', '[]')) e;
599
+ insert into meta.button
600
+ 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')))).*
601
+ from jsonb_array_elements(coalesce(v_page->'buttons', '[]')) e;
602
+ insert into meta.dynamic_action
603
+ 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')))).*
604
+ from jsonb_array_elements(coalesce(v_page->'dynamic_actions', '[]')) e;
605
+ insert into meta.validation
606
+ select (jsonb_populate_record(null::meta.validation, e || jsonb_build_object('id', nextval('meta.validation_id_seq'), 'page_id', v_page_id))).*
607
+ from jsonb_array_elements(coalesce(v_page->'validations', '[]')) e;
608
+ insert into meta.process
609
+ 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')))).*
610
+ from jsonb_array_elements(coalesce(v_page->'processes', '[]')) e;
611
+ insert into meta.computation
612
+ select (jsonb_populate_record(null::meta.computation, e || jsonb_build_object('id', nextval('meta.computation_id_seq'), 'page_id', v_page_id))).*
613
+ from jsonb_array_elements(coalesce(v_page->'computations', '[]')) e;
614
+ insert into meta.branch
615
+ select (jsonb_populate_record(null::meta.branch, e || jsonb_build_object('id', nextval('meta.branch_id_seq'), 'page_id', v_page_id))).*
616
+ from jsonb_array_elements(coalesce(v_page->'branches', '[]')) e;
617
+ end loop;
618
+
619
+ return v_app_id;
620
+ end
621
+ $$;