pgkiln 0.31.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +2 -2
  2. package/SECURITY.md +10 -0
  3. package/db/migrations/076_form_lost_update.sql +50 -0
  4. package/db/migrations/077_warn_unsaved.sql +41 -0
  5. package/db/migrations/078_url_region.sql +12 -0
  6. package/db/migrations/079_developer_toolbar.sql +53 -0
  7. package/db/migrations/080_search.sql +326 -0
  8. package/db/migrations/081_mail.sql +621 -0
  9. package/db/migrations/082_app_backups.sql +73 -0
  10. package/db/migrations/083_availability.sql +156 -0
  11. package/db/migrations/084_feedback.sql +46 -0
  12. package/db/migrations/085_page_behaviour.sql +65 -0
  13. package/db/migrations/086_data_settings.sql +599 -0
  14. package/db/migrations/087_login_page.sql +175 -0
  15. package/db/migrations/088_region_templates.sql +18 -0
  16. package/db/migrations/089_page_groups.sql +330 -0
  17. package/docs/apex-feature-parity.md +61 -39
  18. package/docs/guide/01-installation.md +1 -0
  19. package/docs/guide/03-builder.md +154 -4
  20. package/docs/guide/04-pages-and-regions.md +157 -3
  21. package/docs/guide/05-items.md +47 -0
  22. package/docs/guide/06-processing.md +210 -2
  23. package/docs/guide/07-dynamic-actions.md +60 -2
  24. package/docs/guide/08-security.md +56 -4
  25. package/docs/guide/09-reference.md +21 -0
  26. package/docs/guide/11-from-apex.md +9 -2
  27. package/docs/guide/12-development.md +40 -2
  28. package/docs/guide/14-globalization.md +15 -0
  29. package/docs/guide/15-extensions.md +1 -1
  30. package/docs/guide/17-mobile.md +3 -1
  31. package/docs/guide/18-cli.md +32 -1
  32. package/docs/guide/20-ai-agents.md +5 -2
  33. package/examples/hr/hr_50_search.sql +47 -0
  34. package/examples/hr/hr_51_mail.sql +70 -0
  35. package/examples/hr/hr_52_feedback.sql +15 -0
  36. package/examples/hr/hr_53_data_settings.sql +108 -0
  37. package/examples/hr/hr_54_region_templates.sql +67 -0
  38. package/examples/hr/hr_55_page_groups.sql +24 -0
  39. package/package.json +3 -1
  40. package/public/app.css +122 -3
  41. package/public/app.js +496 -13
  42. package/public/builder.css +22 -0
  43. package/public/builder.js +77 -0
  44. package/public/code-editor.css +3 -3
  45. package/public/sw.js +5 -3
  46. package/src/app.ts +10 -0
  47. package/src/appfiles.ts +13 -0
  48. package/src/automations.ts +12 -0
  49. package/src/builder/advisor.ts +68 -14
  50. package/src/builder/arrange.ts +2 -2
  51. package/src/builder/availability.ts +74 -0
  52. package/src/builder/backups.ts +134 -0
  53. package/src/builder/components.ts +123 -16
  54. package/src/builder/ddl-tools.ts +114 -0
  55. package/src/builder/designer.ts +35 -4
  56. package/src/builder/feedback.ts +82 -0
  57. package/src/builder/forms.ts +10 -5
  58. package/src/builder/home.ts +3 -0
  59. package/src/builder/locks.ts +15 -5
  60. package/src/builder/login-page.ts +82 -0
  61. package/src/builder/mail.ts +425 -0
  62. package/src/builder/newapp.ts +1 -1
  63. package/src/builder/region-help.ts +73 -0
  64. package/src/builder/region-settings.ts +19 -3
  65. package/src/builder/routes.ts +60 -5
  66. package/src/builder/search.ts +4 -4
  67. package/src/builder/shared.ts +3 -2
  68. package/src/builder/sql.ts +3 -0
  69. package/src/builder/ui.ts +9 -3
  70. package/src/cli/apps.ts +46 -5
  71. package/src/cli/main.ts +66 -5
  72. package/src/cli/mcp.ts +58 -4
  73. package/src/cli/replace.ts +40 -4
  74. package/src/ddl.ts +509 -0
  75. package/src/geocode.ts +44 -0
  76. package/src/i18n/ar.ts +56 -0
  77. package/src/i18n/cs.ts +56 -0
  78. package/src/i18n/da.ts +56 -0
  79. package/src/i18n/de.ts +56 -0
  80. package/src/i18n/el.ts +56 -0
  81. package/src/i18n/es.ts +56 -0
  82. package/src/i18n/fi.ts +56 -0
  83. package/src/i18n/fr.ts +56 -0
  84. package/src/i18n/he.ts +56 -0
  85. package/src/i18n/it.ts +56 -0
  86. package/src/i18n/ja.ts +56 -0
  87. package/src/i18n/ko.ts +56 -0
  88. package/src/i18n/nb.ts +56 -0
  89. package/src/i18n/pl.ts +56 -0
  90. package/src/i18n/pt.ts +56 -0
  91. package/src/i18n/ru.ts +56 -0
  92. package/src/i18n/sv.ts +56 -0
  93. package/src/i18n/tr.ts +56 -0
  94. package/src/i18n/uk.ts +56 -0
  95. package/src/i18n/zh.ts +56 -0
  96. package/src/i18n.ts +112 -0
  97. package/src/icons.ts +2 -1
  98. package/src/layout.ts +3 -2
  99. package/src/mail/mail.ts +148 -0
  100. package/src/mail/providers.ts +444 -0
  101. package/src/metadata.ts +34 -10
  102. package/src/runtime/ajax.ts +97 -0
  103. package/src/runtime/authz.ts +1 -1
  104. package/src/runtime/availability.ts +37 -0
  105. package/src/runtime/context.ts +46 -6
  106. package/src/runtime/data-export.ts +94 -0
  107. package/src/runtime/data-items.ts +194 -0
  108. package/src/runtime/devbar.ts +184 -0
  109. package/src/runtime/display-selector.ts +12 -4
  110. package/src/runtime/engine.ts +26 -1
  111. package/src/runtime/feedback.ts +128 -0
  112. package/src/runtime/grid.ts +170 -26
  113. package/src/runtime/items.ts +8 -0
  114. package/src/runtime/lists.ts +11 -1
  115. package/src/runtime/password-reset.ts +171 -0
  116. package/src/runtime/processes.ts +83 -2
  117. package/src/runtime/pwa.ts +7 -10
  118. package/src/runtime/regions.ts +41 -0
  119. package/src/runtime/render.ts +48 -7
  120. package/src/runtime/report-views.ts +4 -2
  121. package/src/runtime/report.ts +36 -22
  122. package/src/runtime/rest.ts +4 -0
  123. package/src/runtime/routes.ts +94 -37
  124. package/src/runtime/search.ts +252 -0
  125. package/src/runtime/signin.ts +80 -0
  126. package/src/runtime/url-region.ts +93 -0
  127. package/src/security.ts +11 -1
  128. package/src/server.ts +2 -0
  129. package/src/session.ts +8 -2
  130. package/src/static-version.ts +34 -0
  131. package/src/workingcopy.ts +2 -2
@@ -0,0 +1,599 @@
1
+ -- =====================================================================
2
+ -- 086: data and settings (sprint 53)
3
+ --
4
+ -- meta.app_setting (APEX: Application Settings, APEX_APP_SETTING): named
5
+ -- values of an application that an administrator changes after
6
+ -- deployment, read and written from SQL with meta.get_app_setting(name) and
7
+ -- meta.set_app_setting(name, value). Exported with the application; an
8
+ -- import over an existing application keeps the installed values of the
9
+ -- settings marked keep_on_upgrade.
10
+ --
11
+ -- meta.user_preference (APEX: APEX_UTIL.GET/SET/REMOVE_PREFERENCE): values
12
+ -- kept per user and application across sessions. Items with
13
+ -- {"persist": "user"} (APEX: Maintain session state: Per User) are read
14
+ -- from and saved to them by the runtime. Installation data: not exported.
15
+ --
16
+ -- meta.dynamic_translation (APEX: Dynamic Translations, APEX_LANG.LANG):
17
+ -- translations of values from tables (e.g. list of values entries):
18
+ -- meta.lang(text) returns the text in the session's language.
19
+ --
20
+ -- meta.data_export(query, format, options) (APEX: APEX_DATA_EXPORT): a
21
+ -- query's rows as a CSV, JSON, XML or HTML file (bytea), run with the
22
+ -- caller's privileges. The download process makes Excel and PDF too.
23
+ --
24
+ -- Item types image (APEX: Display Image: a URL, or a BLOB from a query) and
25
+ -- address (APEX: Geocoded Address: an address checked against the
26
+ -- installation's geocoder, its position into another item).
27
+ --
28
+ -- export_app / import_app are redefined from 083 / 081 to carry
29
+ -- app_settings and dynamic_translations.
30
+ -- =====================================================================
31
+
32
+ -- The current application, when called as its own database role (application code).
33
+ create function meta.caller_app(p_fn text) returns int
34
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
35
+ declare
36
+ v_app int := meta.app_id();
37
+ v_role text;
38
+ begin
39
+ if v_app is null then
40
+ raise exception '%: no current application (call it from application code)', p_fn;
41
+ end if;
42
+ select a.db_role into v_role from meta.app a where a.id = v_app;
43
+ if not found then
44
+ raise exception '%: no current application (call it from application code)', p_fn;
45
+ end if;
46
+ -- an application without a role of its own runs as the runtime's login role (no SET ROLE):
47
+ -- another application's role can't claim it
48
+ if current_setting('role') is distinct from coalesce(v_role, 'none') then
49
+ raise exception '%: called outside the application''s own database role', p_fn;
50
+ end if;
51
+ return v_app;
52
+ end
53
+ $$;
54
+ revoke all on function meta.caller_app(text) from public;
55
+
56
+ -- ---------------------------------------------------------------- application settings
57
+ create table meta.app_setting (
58
+ id serial primary key,
59
+ app_id int not null references meta.app on delete cascade,
60
+ name text not null check (name ~ '^[A-Z][A-Z0-9_]{0,62}$'),
61
+ value text check (octet_length(value) <= 4000),
62
+ description text,
63
+ -- a value is required (an empty one is refused)
64
+ required boolean not null default false,
65
+ -- the values allowed (null: any)
66
+ valid_values text[] check (valid_values is null or cardinality(valid_values) between 1 and 100),
67
+ -- an import over an installed application keeps the installed value
68
+ keep_on_upgrade boolean not null default true,
69
+ unique (app_id, name),
70
+ constraint app_setting_required check (not required or coalesce(value, '') <> ''),
71
+ constraint app_setting_valid check (value is null or valid_values is null or value = any(valid_values))
72
+ );
73
+ revoke all on meta.app_setting from public;
74
+ comment on table meta.app_setting is 'Application settings (086): named values changed after deployment; meta.get_app_setting / meta.set_app_setting';
75
+
76
+ -- an export made before 086 (or a document written by hand) may leave the flags out
77
+ create function meta.app_setting_defaults() returns trigger
78
+ language plpgsql set search_path = pg_catalog as $$
79
+ begin
80
+ new.required := coalesce(new.required, false);
81
+ new.keep_on_upgrade := coalesce(new.keep_on_upgrade, true);
82
+ return new;
83
+ end $$;
84
+ create trigger app_setting_defaults before insert on meta.app_setting
85
+ for each row execute function meta.app_setting_defaults();
86
+
87
+ -- APEX_APP_SETTING.GET_VALUE
88
+ create function meta.get_app_setting(p_name text) returns text
89
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
90
+ declare
91
+ v_app int := meta.caller_app('meta.get_app_setting');
92
+ v meta.app_setting;
93
+ begin
94
+ select * into v from meta.app_setting where app_id = v_app and name = upper(p_name);
95
+ if not found then
96
+ raise exception 'meta.get_app_setting: the application has no setting %', upper(p_name);
97
+ end if;
98
+ return v.value;
99
+ end
100
+ $$;
101
+ grant execute on function meta.get_app_setting(text) to public;
102
+
103
+ -- APEX_APP_SETTING.SET_VALUE
104
+ create function meta.set_app_setting(p_name text, p_value text) returns void
105
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
106
+ declare
107
+ v_app int := meta.caller_app('meta.set_app_setting');
108
+ v meta.app_setting;
109
+ begin
110
+ select * into v from meta.app_setting where app_id = v_app and name = upper(p_name) for update;
111
+ if not found then
112
+ raise exception 'meta.set_app_setting: the application has no setting %', upper(p_name);
113
+ end if;
114
+ if v.required and coalesce(p_value, '') = '' then
115
+ raise exception 'meta.set_app_setting: % needs a value', v.name;
116
+ end if;
117
+ if p_value is not null and v.valid_values is not null and not p_value = any(v.valid_values) then
118
+ raise exception 'meta.set_app_setting: % is one of %', v.name, array_to_string(v.valid_values, ', ');
119
+ end if;
120
+ update meta.app_setting set value = p_value where id = v.id;
121
+ end
122
+ $$;
123
+ grant execute on function meta.set_app_setting(text, text) to public;
124
+
125
+ -- ---------------------------------------------------------------- user preferences
126
+ create table meta.user_preference (
127
+ app_id int not null references meta.app on delete cascade,
128
+ username text not null,
129
+ name text not null check (name ~ '^[A-Za-z][A-Za-z0-9_.-]{0,99}$'),
130
+ value text check (octet_length(value) <= 32000),
131
+ updated_at timestamptz not null default now(),
132
+ primary key (app_id, username, name)
133
+ );
134
+ revoke all on meta.user_preference from public;
135
+ comment on table meta.user_preference is 'User preferences (086): values kept per user across sessions; meta.get_preference / set_preference / remove_preference';
136
+
137
+ -- The user a preference belongs to: the given one, or the signed-in user.
138
+ create function meta.preference_user(p_fn text, p_user text) returns text
139
+ language plpgsql stable set search_path = meta, pg_catalog as $$
140
+ declare
141
+ v_user text := coalesce(nullif(btrim(p_user), ''), nullif(current_setting('pgkiln.app_user', true), ''));
142
+ begin
143
+ if v_user is null or v_user = 'nobody' then
144
+ raise exception '%: no signed-in user', p_fn;
145
+ end if;
146
+ return v_user;
147
+ end
148
+ $$;
149
+ revoke all on function meta.preference_user(text, text) from public;
150
+
151
+ -- APEX_UTIL.GET_PREFERENCE: null when the user has none
152
+ create function meta.get_preference(p_name text, p_user text default null) returns text
153
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
154
+ declare
155
+ v_app int := meta.caller_app('meta.get_preference');
156
+ v_user text := coalesce(nullif(btrim(p_user), ''), nullif(current_setting('pgkiln.app_user', true), ''));
157
+ begin
158
+ if v_user is null or v_user = 'nobody' then
159
+ return null;
160
+ end if;
161
+ return (select value from meta.user_preference where app_id = v_app and username = v_user and name = p_name);
162
+ end
163
+ $$;
164
+ grant execute on function meta.get_preference(text, text) to public;
165
+
166
+ -- APEX_UTIL.SET_PREFERENCE: at most 500 preferences per user and application
167
+ create function meta.set_preference(p_name text, p_value text, p_user text default null) returns void
168
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
169
+ declare
170
+ v_app int := meta.caller_app('meta.set_preference');
171
+ v_user text := meta.preference_user('meta.set_preference', p_user);
172
+ begin
173
+ if p_name is null or p_name !~ '^[A-Za-z][A-Za-z0-9_.-]{0,99}$' then
174
+ raise exception 'meta.set_preference: a name of letters, digits, _ . and - (at most 100)';
175
+ end if;
176
+ if not exists (select 1 from meta.user_preference where app_id = v_app and username = v_user and name = p_name)
177
+ and (select count(*) from meta.user_preference where app_id = v_app and username = v_user) >= 500 then
178
+ raise exception 'meta.set_preference: at most 500 preferences per user';
179
+ end if;
180
+ insert into meta.user_preference (app_id, username, name, value) values (v_app, v_user, p_name, p_value)
181
+ on conflict (app_id, username, name) do update set value = excluded.value, updated_at = now();
182
+ end
183
+ $$;
184
+ grant execute on function meta.set_preference(text, text, text) to public;
185
+
186
+ -- APEX_UTIL.REMOVE_PREFERENCE
187
+ create function meta.remove_preference(p_name text, p_user text default null) returns void
188
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
189
+ declare
190
+ v_app int := meta.caller_app('meta.remove_preference');
191
+ v_user text := meta.preference_user('meta.remove_preference', p_user);
192
+ begin
193
+ delete from meta.user_preference where app_id = v_app and username = v_user and name = p_name;
194
+ end
195
+ $$;
196
+ grant execute on function meta.remove_preference(text, text) to public;
197
+
198
+ -- ---------------------------------------------------------------- dynamic translations
199
+ create table meta.dynamic_translation (
200
+ id serial primary key,
201
+ app_id int not null references meta.app on delete cascade,
202
+ language text not null check (language ~ '^[a-z]{2,3}(-[A-Za-z0-9]{2,8}){0,2}$'),
203
+ source text not null check (length(source) between 1 and 4000),
204
+ target text not null check (length(target) <= 4000),
205
+ unique (app_id, language, source)
206
+ );
207
+ revoke all on meta.dynamic_translation from public;
208
+ comment on table meta.dynamic_translation is 'Dynamic translations (086): values from tables in other languages; meta.lang(text)';
209
+
210
+ -- APEX_LANG.LANG: the text in the session's language (or p_language): an exact
211
+ -- match, then the base language (nl for nl-BE), else the text as it is.
212
+ create function meta.lang(p_text text, p_language text default null) returns text
213
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
214
+ declare
215
+ v_app int := meta.app_id();
216
+ v_lang text := lower(coalesce(nullif(btrim(p_language), ''), meta.app_language()));
217
+ v_out text;
218
+ begin
219
+ if p_text is null or v_app is null then
220
+ return p_text;
221
+ end if;
222
+ select t.target into v_out from meta.dynamic_translation t
223
+ where t.app_id = v_app and t.source = p_text and t.language in (v_lang, split_part(v_lang, '-', 1))
224
+ order by t.language = v_lang desc
225
+ limit 1;
226
+ return coalesce(v_out, p_text);
227
+ end
228
+ $$;
229
+ grant execute on function meta.lang(text, text) to public;
230
+
231
+ -- ---------------------------------------------------------------- data export
232
+ -- A CSV field, as the report download writes it: quoted when it holds the
233
+ -- separator, a quote or a line break; text that looks like a formula
234
+ -- (= + - @ tab CR) gets a leading apostrophe so a spreadsheet doesn't run it.
235
+ create function meta.csv_field(p_value text, p_sep text default ',', p_text boolean default true) returns text
236
+ language sql immutable set search_path = pg_catalog as $$
237
+ select case
238
+ when v is null then ''
239
+ when strpos(v, p_sep) > 0 or v ~ '["\r\n]' then '"' || replace(v, '"', '""') || '"'
240
+ else v
241
+ end
242
+ from (select case when p_text and p_value ~ '^[=+\-@\t\r]' then '''' || p_value else p_value end as v) x
243
+ $$;
244
+ grant execute on function meta.csv_field(text, text, boolean) to public;
245
+
246
+ -- APEX_DATA_EXPORT.EXPORT: the rows of a query as a file. Formats: csv,
247
+ -- json (an array of objects), xml (query_to_xml) and html (a table).
248
+ -- Options: {"separator": ";", "header": false, "bom": false, "title": "…"}.
249
+ -- Runs with the caller's privileges (security invoker): row level security
250
+ -- and grants apply as to the query itself.
251
+ create function meta.data_export(p_query text, p_format text default 'csv', p_options jsonb default '{}') returns bytea
252
+ language plpgsql volatile as $$
253
+ declare
254
+ v_format text := lower(coalesce(nullif(btrim(p_format), ''), 'csv'));
255
+ v_opts jsonb := coalesce(p_options, '{}');
256
+ v_query text := regexp_replace(coalesce(p_query, ''), '[\s;]+$', '');
257
+ v_sep text := coalesce(v_opts->>'separator', ',');
258
+ v_header boolean := coalesce((v_opts->>'header')::boolean, true);
259
+ v_cols text[];
260
+ v_rows text;
261
+ v_title text := v_opts->>'title';
262
+ begin
263
+ if btrim(v_query) = '' then
264
+ raise exception 'meta.data_export: a query is required';
265
+ end if;
266
+ if v_format not in ('csv', 'json', 'xml', 'html') then
267
+ raise exception 'meta.data_export: the format is csv, json, xml or html (Excel and PDF: the download process)';
268
+ end if;
269
+ if length(v_sep) <> 1 or v_sep ~ '["\r\n]' then
270
+ raise exception 'meta.data_export: the separator is one character';
271
+ end if;
272
+ if v_format = 'json' then
273
+ execute format(E'select coalesce(json_agg(q), ''[]'')::text from (\n%s\n) q', v_query) into v_rows;
274
+ return convert_to(v_rows, 'UTF8');
275
+ end if;
276
+ if v_format = 'xml' then
277
+ return convert_to(query_to_xml(v_query, true, false, '')::text, 'UTF8');
278
+ end if;
279
+ -- the column names, in order, also when there are no rows (the outer join on false reads no row)
280
+ execute format(E'select array(select json_object_keys(row_to_json(x))) from (select q.* from (select 1) d left join (\n%s\n) q on false) x', v_query)
281
+ into v_cols;
282
+ if v_format = 'csv' then
283
+ execute format(E'select string_agg((select string_agg(meta.csv_field(e.value #>> ''{}'', %L, json_typeof(e.value) = ''string''), %L order by e.n) from json_each(row_to_json(q)) with ordinality e(k, value, n)), E''\\r\\n'') from (\n%s\n) q',
284
+ v_sep, v_sep, v_query)
285
+ into v_rows;
286
+ return convert_to(
287
+ case when coalesce((v_opts->>'bom')::boolean, true) then chr(65279) else '' end
288
+ || case when v_header then (select string_agg(meta.csv_field(c, v_sep), v_sep order by n) from unnest(v_cols) with ordinality u(c, n)) || E'\r\n' else '' end
289
+ || coalesce(v_rows || E'\r\n', ''),
290
+ 'UTF8');
291
+ end if;
292
+ -- html
293
+ execute format(E'select string_agg(''<tr>'' || (select string_agg(''<td>'' || coalesce(meta.html_escape(e.value), '''') || ''</td>'', '''' order by e.n) from json_each_text(row_to_json(q)) with ordinality e(k, value, n)) || ''</tr>'', E''\\n'') from (\n%s\n) q',
294
+ v_query)
295
+ into v_rows;
296
+ return convert_to(
297
+ '<!doctype html>' || E'\n' || '<html><head><meta charset="utf-8"><title>' || coalesce(meta.html_escape(v_title), 'Export') || '</title></head><body>' || E'\n'
298
+ || case when v_title is not null then '<h1>' || meta.html_escape(v_title) || '</h1>' || E'\n' else '' end
299
+ || '<table>' || E'\n'
300
+ || case when v_header then '<thead><tr>' || coalesce((select string_agg('<th>' || meta.html_escape(c) || '</th>', '' order by n) from unnest(v_cols) with ordinality u(c, n)), '') || '</tr></thead>' || E'\n' else '' end
301
+ || '<tbody>' || E'\n' || coalesce(v_rows || E'\n', '') || '</tbody></table>' || E'\n' || '</body></html>' || E'\n',
302
+ 'UTF8');
303
+ end
304
+ $$;
305
+ grant execute on function meta.data_export(text, text, jsonb) to public;
306
+
307
+ -- ---------------------------------------------------------------- items
308
+ alter table meta.item drop constraint item_type_check;
309
+ alter table meta.item add constraint item_type_check check (type in (
310
+ 'text', 'textarea', 'number', 'date', 'datetime', 'select', 'radio', 'checkbox', 'switch', 'hidden', 'display', 'password',
311
+ 'checkbox_group', 'multiselect', 'popup_lov', 'email', 'tel', 'url', 'color', 'file', 'location', 'richtext', 'markdown',
312
+ 'rating', 'combobox', 'daterange', 'qrcode', 'plugin', 'image', 'address'));
313
+
314
+ -- ---------------------------------------------------------------- export
315
+ -- Same as 083, plus app_settings and dynamic_translations.
316
+ create or replace function meta.export_app(p_alias text) returns jsonb
317
+ language sql stable set search_path = meta, pg_catalog as $$
318
+ select jsonb_build_object(
319
+ 'format', 'pgkiln/2',
320
+ -- (083) availability and build status belong to the installation
321
+ 'app', to_jsonb(a) - 'id' - 'created_at' - 'updated_at' - 'availability' - 'availability_message' - 'availability_users' - 'build_status',
322
+ '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), '[]'),
323
+ '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), '[]'),
324
+ '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), '[]'),
325
+ '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), '[]'),
326
+ '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), '[]'),
327
+ '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), '[]'),
328
+ '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), '[]'),
329
+ '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)
330
+ from meta.report_layout x where x.app_id = a.id), '[]'),
331
+ 'automations', coalesce((select jsonb_agg(to_jsonb(x) - 'id' - 'app_id' - 'next_run_at' - 'last_run_at' - 'last_status' - 'code' order by x.name)
332
+ from meta.automation x where x.app_id = a.id), '[]'),
333
+ -- (044) the actions of the automations, by automation name
334
+ '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")
335
+ from meta.automation_action x where x.app_id = a.id), '[]'),
336
+ '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), '[]'),
337
+ '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), '[]'),
338
+ '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), '[]'),
339
+ '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), '[]'),
340
+ '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), '[]'),
341
+ '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), '[]'),
342
+ -- (030) secrets never leave the installation: they are entered again after an import
343
+ -- (050) nor do the password and refresh token of the OAuth2 flows, or the token state
344
+ '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)
345
+ from meta.web_credential x where x.app_id = a.id), '[]'),
346
+ -- (050) the synchronisation's state (next and last run) belongs to the installation
347
+ '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)
348
+ from meta.rest_source x where x.app_id = a.id), '[]'),
349
+ -- (041) data load definitions
350
+ '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), '[]'),
351
+ -- (042) lists; entries keep their ids so parents can be remapped on import
352
+ '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), '[]'),
353
+ '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), '[]'),
354
+ -- (042) supporting objects: install, upgrade and deinstall scripts (never run on import)
355
+ '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), '[]'),
356
+ -- (069) plug-ins (their files are static files, their templates template components)
357
+ '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), '[]'),
358
+ -- (080) search configurations
359
+ '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), '[]'),
360
+ -- (081) e-mail templates
361
+ '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), '[]'),
362
+ -- (086) application settings (with their values) and dynamic translations
363
+ '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), '[]'),
364
+ '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), '[]'),
365
+ -- (068) static application files (JavaScript, CSS, images, fonts) as base64
366
+ '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)
367
+ from meta.static_file x where x.app_id = a.id), '[]'),
368
+ -- nav entries and regions keep their ids, so parents and references can be remapped on import
369
+ '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), '[]'),
370
+ 'pages', coalesce((
371
+ select jsonb_agg(to_jsonb(p) - 'id' - 'app_id' || jsonb_build_object(
372
+ '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), '[]'),
373
+ '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), '[]'),
374
+ '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), '[]'),
375
+ '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), '[]'),
376
+ '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), '[]'),
377
+ '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), '[]'),
378
+ '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), '[]'),
379
+ '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), '[]')
380
+ ) order by p.page_no)
381
+ from meta.page p where p.app_id = a.id), '[]'))
382
+ from meta.app a
383
+ where a.alias = p_alias
384
+ $$;
385
+
386
+ -- ---------------------------------------------------------------- import
387
+ -- Same as 081, plus app_settings and dynamic_translations.
388
+ create or replace function meta.import_app(p_doc jsonb, p_alias text default null) returns int
389
+ language plpgsql set search_path = meta, pg_catalog as $$
390
+ declare
391
+ v_app_id int;
392
+ v_page_id int;
393
+ v_page jsonb;
394
+ v_e jsonb;
395
+ v_rmap jsonb;
396
+ v_nmap jsonb := '{}';
397
+ v_lmap jsonb := '{}';
398
+ v_new_id int;
399
+ begin
400
+ if p_doc->>'format' is distinct from 'pgkiln/2' then
401
+ raise exception 'unsupported export format %', coalesce(p_doc->>'format', '(none)');
402
+ end if;
403
+
404
+ insert into meta.app
405
+ select (jsonb_populate_record(null::meta.app, p_doc->'app' || jsonb_build_object(
406
+ 'id', nextval('meta.app_id_seq'),
407
+ 'alias', coalesce(p_alias, p_doc->'app'->>'alias'),
408
+ 'created_at', now(), 'updated_at', now()))).*
409
+ returning id into v_app_id;
410
+
411
+ insert into meta.authz_scheme
412
+ select (jsonb_populate_record(null::meta.authz_scheme, e || jsonb_build_object('id', nextval('meta.authz_scheme_id_seq'), 'app_id', v_app_id))).*
413
+ from jsonb_array_elements(coalesce(p_doc->'authz_schemes', '[]')) e;
414
+ insert into meta.app_item
415
+ select (jsonb_populate_record(null::meta.app_item, e || jsonb_build_object('id', nextval('meta.app_item_id_seq'), 'app_id', v_app_id))).*
416
+ from jsonb_array_elements(coalesce(p_doc->'app_items', '[]')) e;
417
+ insert into meta.app_process
418
+ select (jsonb_populate_record(null::meta.app_process, e || jsonb_build_object('id', nextval('meta.app_process_id_seq'), 'app_id', v_app_id))).*
419
+ from jsonb_array_elements(coalesce(p_doc->'app_processes', '[]')) e;
420
+ insert into meta.lov
421
+ select (jsonb_populate_record(null::meta.lov, e || jsonb_build_object('id', nextval('meta.lov_id_seq'), 'app_id', v_app_id))).*
422
+ from jsonb_array_elements(coalesce(p_doc->'lovs', '[]')) e;
423
+ insert into meta.app_group_role
424
+ select (jsonb_populate_record(null::meta.app_group_role, e || jsonb_build_object('app_id', v_app_id))).*
425
+ from jsonb_array_elements(coalesce(p_doc->'group_roles', '[]')) e;
426
+ insert into meta.text_message
427
+ select (jsonb_populate_record(null::meta.text_message, e || jsonb_build_object('app_id', v_app_id))).*
428
+ from jsonb_array_elements(coalesce(p_doc->'text_messages', '[]')) e;
429
+ insert into meta.translation
430
+ select (jsonb_populate_record(null::meta.translation, e || jsonb_build_object('app_id', v_app_id))).*
431
+ from jsonb_array_elements(coalesce(p_doc->'translations', '[]')) e;
432
+ insert into meta.report_layout
433
+ select (jsonb_populate_record(null::meta.report_layout, (e - 'logo') || jsonb_build_object(
434
+ 'id', nextval('meta.report_layout_id_seq'), 'app_id', v_app_id,
435
+ 'logo', null))).*
436
+ from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e;
437
+ update meta.report_layout l
438
+ set logo = decode(e->>'logo', 'base64')
439
+ from jsonb_array_elements(coalesce(p_doc->'report_layouts', '[]')) e
440
+ where l.app_id = v_app_id and l.name = e->>'name' and e->>'logo' is not null;
441
+ -- imported automations start switched off: the copy must not run the original's jobs unasked.
442
+ -- (044) a file with actions: the automations' code is ignored; an older file: code becomes the single action
443
+ insert into meta.automation
444
+ select (jsonb_populate_record(null::meta.automation, '{"error_handling": "stop"}'::jsonb
445
+ || jsonb_strip_nulls(case when p_doc ? 'automation_actions' then e - 'code' else e end)
446
+ || jsonb_build_object('id', nextval('meta.automation_id_seq'), 'app_id', v_app_id, 'enabled', false))).*
447
+ from jsonb_array_elements(coalesce(p_doc->'automations', '[]')) e;
448
+ insert into meta.automation_action
449
+ select (jsonb_populate_record(null::meta.automation_action, '{"seq": 10}'::jsonb || jsonb_strip_nulls(e)
450
+ || jsonb_build_object('id', nextval('meta.automation_action_id_seq'), 'app_id', v_app_id))).*
451
+ from jsonb_array_elements(coalesce(p_doc->'automation_actions', '[]')) e;
452
+ insert into meta.document_template
453
+ select (jsonb_populate_record(null::meta.document_template, e || jsonb_build_object('id', nextval('meta.document_template_id_seq'), 'app_id', v_app_id))).*
454
+ from jsonb_array_elements(coalesce(p_doc->'document_templates', '[]')) e;
455
+ insert into meta.task_definition
456
+ select (jsonb_populate_record(null::meta.task_definition, e || jsonb_build_object('id', nextval('meta.task_definition_id_seq'), 'app_id', v_app_id))).*
457
+ from jsonb_array_elements(coalesce(p_doc->'task_definitions', '[]')) e;
458
+ insert into meta.workflow_definition
459
+ select (jsonb_populate_record(null::meta.workflow_definition, e || jsonb_build_object('id', nextval('meta.workflow_definition_id_seq'), 'app_id', v_app_id))).*
460
+ from jsonb_array_elements(coalesce(p_doc->'workflow_definitions', '[]')) e;
461
+ insert into meta.rest_module
462
+ select (jsonb_populate_record(null::meta.rest_module, e || jsonb_build_object('id', nextval('meta.rest_module_id_seq'), 'app_id', v_app_id))).*
463
+ from jsonb_array_elements(coalesce(p_doc->'rest_modules', '[]')) e;
464
+ -- (028) template components: regions and report columns refer to them by static id
465
+ insert into meta.template_component
466
+ select (jsonb_populate_record(null::meta.template_component, e || jsonb_build_object('id', nextval('meta.template_component_id_seq'), 'app_id', v_app_id))).*
467
+ from jsonb_array_elements(coalesce(p_doc->'template_components', '[]')) e;
468
+ -- (029) build options (components name them by name)
469
+ insert into meta.build_option
470
+ select (jsonb_populate_record(null::meta.build_option, e || jsonb_build_object('id', nextval('meta.build_option_id_seq'), 'app_id', v_app_id))).*
471
+ from jsonb_array_elements(coalesce(p_doc->'build_options', '[]')) e;
472
+ -- (030) web credentials arrive without a secret, whatever the document holds
473
+ -- (050) nor a password or refresh token
474
+ insert into meta.web_credential
475
+ select (jsonb_populate_record(null::meta.web_credential, '{"type": "basic", "valid_for": [], "grant_type": "client_credentials"}'::jsonb
476
+ || jsonb_strip_nulls(e - 'secret_enc' - 'password_enc' - 'refresh_token_enc' - 'token_refreshed_at')
477
+ || jsonb_build_object('id', nextval('meta.web_credential_id_seq'), 'app_id', v_app_id))).*
478
+ from jsonb_array_elements(coalesce(p_doc->'web_credentials', '[]')) e;
479
+ -- (050) write-back operations and synchronisation; an imported synchronisation starts switched off
480
+ insert into meta.rest_source
481
+ select (jsonb_populate_record(null::meta.rest_source, '{"method": "GET", "headers": {}, "params": [], "columns": [], "cache_seconds": 0, "timeout_s": 10, "max_rows": 1000,
482
+ "operations": {}, "key_columns": [], "sync_mode": "merge", "sync_delete": false, "sync_time_zone": "UTC"}'::jsonb
483
+ || jsonb_strip_nulls(e - 'sync_next_at' - 'sync_last_at' - 'sync_last_status')
484
+ || jsonb_build_object('id', nextval('meta.rest_source_id_seq'), 'app_id', v_app_id, 'sync_enabled', false))).*
485
+ from jsonb_array_elements(coalesce(p_doc->'rest_sources', '[]')) e;
486
+
487
+ -- (041) data load definitions (the data_load process names them)
488
+ insert into meta.data_load_def
489
+ select (jsonb_populate_record(null::meta.data_load_def, '{"format": "auto", "headers": true, "mode": "append", "skip_errors": false, "columns": []}'::jsonb
490
+ || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.data_load_def_id_seq'), 'app_id', v_app_id))).*
491
+ from jsonb_array_elements(coalesce(p_doc->'data_load_definitions', '[]')) e;
492
+
493
+ -- (042) lists and their entries, supporting scripts. Entries are inserted
494
+ -- without a parent first and the parents set afterwards, so the order of
495
+ -- the entries in the document doesn't matter (a moved entry may have a
496
+ -- higher id than its children).
497
+ insert into meta.list
498
+ 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))).*
499
+ from jsonb_array_elements(coalesce(p_doc->'lists', '[]')) e;
500
+ for v_e in select * from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) loop
501
+ insert into meta.list_entry
502
+ select (jsonb_populate_record(null::meta.list_entry, '{"target_items": {}}'::jsonb || jsonb_strip_nulls(v_e) || jsonb_build_object(
503
+ 'id', nextval('meta.list_entry_id_seq'), 'app_id', v_app_id, 'parent_id', null))).*
504
+ returning id into v_new_id;
505
+ if v_e->>'id' is not null then
506
+ v_lmap := v_lmap || jsonb_build_object(v_e->>'id', v_new_id);
507
+ end if;
508
+ end loop;
509
+ update meta.list_entry x
510
+ set parent_id = (v_lmap->>(e->>'parent_id'))::int
511
+ from jsonb_array_elements(coalesce(p_doc->'list_entries', '[]')) e
512
+ where e->>'parent_id' is not null and v_lmap ? (e->>'parent_id') and v_lmap ? (e->>'id')
513
+ and x.id = (v_lmap->>(e->>'id'))::int;
514
+ insert into meta.supporting_script
515
+ select (jsonb_populate_record(null::meta.supporting_script, '{"kind": "install", "seq": 10, "script": ""}'::jsonb || jsonb_strip_nulls(e)
516
+ || jsonb_build_object('id', nextval('meta.supporting_script_id_seq'), 'app_id', v_app_id))).*
517
+ from jsonb_array_elements(coalesce(p_doc->'supporting_scripts', '[]')) e;
518
+
519
+ -- (069) plug-ins
520
+ insert into meta.plugin
521
+ select (jsonb_populate_record(null::meta.plugin, e || jsonb_build_object('id', nextval('meta.plugin_id_seq'), 'app_id', v_app_id))).*
522
+ from jsonb_array_elements(coalesce(p_doc->'plugins', '[]')) e;
523
+
524
+ -- (080) search configurations (search regions name them)
525
+ insert into meta.search_config
526
+ select (jsonb_populate_record(null::meta.search_config, '{"seq": 10, "source_type": "table", "searchable_columns": [], "max_results": 10}'::jsonb
527
+ || jsonb_strip_nulls(e) || jsonb_build_object('id', nextval('meta.search_config_id_seq'), 'app_id', v_app_id))).*
528
+ from jsonb_array_elements(coalesce(p_doc->'search_configs', '[]')) e;
529
+
530
+ -- (081) e-mail templates (meta.send_mail_template and the send_email process name them)
531
+ insert into meta.email_template
532
+ select (jsonb_populate_record(null::meta.email_template, e || jsonb_build_object('id', nextval('meta.email_template_id_seq'), 'app_id', v_app_id))).*
533
+ from jsonb_array_elements(coalesce(p_doc->'email_templates', '[]')) e;
534
+
535
+ -- (086) application settings and dynamic translations
536
+ insert into meta.app_setting
537
+ select (jsonb_populate_record(null::meta.app_setting, e || jsonb_build_object('id', nextval('meta.app_setting_id_seq'), 'app_id', v_app_id))).*
538
+ from jsonb_array_elements(coalesce(p_doc->'app_settings', '[]')) e;
539
+ insert into meta.dynamic_translation
540
+ select (jsonb_populate_record(null::meta.dynamic_translation, e || jsonb_build_object('id', nextval('meta.dynamic_translation_id_seq'), 'app_id', v_app_id))).*
541
+ from jsonb_array_elements(coalesce(p_doc->'dynamic_translations', '[]')) e;
542
+
543
+ -- (068) static application files
544
+ insert into meta.static_file (app_id, name, mime, content, updated_at)
545
+ select v_app_id, e->>'name', e->>'mime', decode(coalesce(e->>'content', ''), 'base64'), now()
546
+ from jsonb_array_elements(coalesce(p_doc->'static_files', '[]')) e;
547
+
548
+ for v_e in select * from jsonb_array_elements(coalesce(p_doc->'nav', '[]')) loop
549
+ insert into meta.nav_entry
550
+ select (jsonb_populate_record(null::meta.nav_entry, v_e || jsonb_build_object(
551
+ 'id', nextval('meta.nav_entry_id_seq'), 'app_id', v_app_id,
552
+ 'parent_id', v_nmap->>(v_e->>'parent_id')))).*
553
+ returning id into v_new_id;
554
+ v_nmap := v_nmap || jsonb_build_object(v_e->>'id', v_new_id);
555
+ end loop;
556
+
557
+ for v_page in select * from jsonb_array_elements(coalesce(p_doc->'pages', '[]')) loop
558
+ insert into meta.page
559
+ select (jsonb_populate_record(null::meta.page, v_page || jsonb_build_object('id', nextval('meta.page_id_seq'), 'app_id', v_app_id))).*
560
+ returning id into v_page_id;
561
+
562
+ v_rmap := '{}';
563
+ for v_e in select * from jsonb_array_elements(coalesce(v_page->'regions', '[]')) loop
564
+ insert into meta.region
565
+ select (jsonb_populate_record(null::meta.region, v_e || jsonb_build_object('id', nextval('meta.region_id_seq'), 'page_id', v_page_id))).*
566
+ returning id into v_new_id;
567
+ v_rmap := v_rmap || jsonb_build_object(v_e->>'id', v_new_id);
568
+ end loop;
569
+ -- facet and map regions point at their report region by id
570
+ update meta.region r
571
+ set config = jsonb_set(r.config, '{report}', to_jsonb((v_rmap->>(r.config->>'report'))::int))
572
+ where r.page_id = v_page_id and r.type in ('facets', 'smart_filters', 'map') and v_rmap ? (r.config->>'report');
573
+
574
+ insert into meta.item
575
+ 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')))).*
576
+ from jsonb_array_elements(coalesce(v_page->'items', '[]')) e;
577
+ insert into meta.button
578
+ 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')))).*
579
+ from jsonb_array_elements(coalesce(v_page->'buttons', '[]')) e;
580
+ insert into meta.dynamic_action
581
+ 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')))).*
582
+ from jsonb_array_elements(coalesce(v_page->'dynamic_actions', '[]')) e;
583
+ insert into meta.validation
584
+ select (jsonb_populate_record(null::meta.validation, e || jsonb_build_object('id', nextval('meta.validation_id_seq'), 'page_id', v_page_id))).*
585
+ from jsonb_array_elements(coalesce(v_page->'validations', '[]')) e;
586
+ insert into meta.process
587
+ 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')))).*
588
+ from jsonb_array_elements(coalesce(v_page->'processes', '[]')) e;
589
+ insert into meta.computation
590
+ select (jsonb_populate_record(null::meta.computation, e || jsonb_build_object('id', nextval('meta.computation_id_seq'), 'page_id', v_page_id))).*
591
+ from jsonb_array_elements(coalesce(v_page->'computations', '[]')) e;
592
+ insert into meta.branch
593
+ select (jsonb_populate_record(null::meta.branch, e || jsonb_build_object('id', nextval('meta.branch_id_seq'), 'page_id', v_page_id))).*
594
+ from jsonb_array_elements(coalesce(v_page->'branches', '[]')) e;
595
+ end loop;
596
+
597
+ return v_app_id;
598
+ end
599
+ $$;